diff --git a/docs/mcp-instance-runtime.md b/docs/mcp-instance-runtime.md
index e86e81d8..bdb4e0b9 100644
--- a/docs/mcp-instance-runtime.md
+++ b/docs/mcp-instance-runtime.md
@@ -107,7 +107,7 @@ That response means the instance runtime path is working and correctly refusing
## MCP Tool Groups
-The server currently has 136 tool registrations across `workers/mcp/src`.
+The server currently has 137 tool registrations across `workers/mcp/src`.
Some are capability-gated and appear only for users with matching agent surfaces, so
`tools/list` on your own connection is the authoritative surface — 19 of those
registrations are gated. The `/health` marker reports the same total from
@@ -179,7 +179,7 @@ Status: active
Verified live:
-- MCP exposed its tool set (26 at the time of this 2026-06-11 record; 136 tool registrations in source today — see MCP Tool Groups above).
+- MCP exposed its tool set (26 at the time of this 2026-06-11 record; 137 tool registrations in source today — see MCP Tool Groups above).
- Required instance tools were present.
- `subscribe_agent` returned the existing active instance.
- `add_instance_knowledge` saved a document to that private instance.
diff --git a/platform-docs/mcp.md b/platform-docs/mcp.md
index 0efd1e79..2db76d2a 100644
--- a/platform-docs/mcp.md
+++ b/platform-docs/mcp.md
@@ -107,7 +107,7 @@ Confirm before destructive actions.
## What `initialize` Answers
-- `serverInfo.version`: `0.1.6`
+- `serverInfo.version`: `0.1.7`
That is the same value the published MCP-registry manifest (`server.json`) carries, and both
are read from one constant — `MCP_SERVER_VERSION` in `workers/mcp/src/server-version.ts` —
@@ -196,7 +196,7 @@ The two published hints are **derived, not hand-maintained per tool**.
`workers/mcp/src/tool-metadata.ts` classifies every tool `read` / `write` / `runtime` /
`destructive` in one table, and `annotationsFor()` maps that classification onto the two
hints. The classification is then derived **back out of the handlers** by `index.test.ts`,
-which drives all 136 tools under two different scope sets and reads the required scope out
+which drives all 137 tools under two different scope sets and reads the required scope out
of each refusal — so a tool announced read-only that enforces a write gate fails the build
rather than reaching a host. `conformance.test.ts` asserts the same thing against a real
`tools/list` response.
@@ -318,7 +318,7 @@ More recipes, with real argument names, are in
## Tool Surface
-The server registers **136 tools**. 117 are always present. The remaining 19 are gated to
+The server registers **137 tools**. 118 are always present. The remaining 19 are gated to
the console surfaces of the connected user's own subscribed agents, so the surface is
per-connection:
diff --git a/server.json b/server.json
index a515c05b..8ed07e48 100644
--- a/server.json
+++ b/server.json
@@ -3,7 +3,7 @@
"name": "io.github.ProAgentStore/platform",
"title": "ProAgentStore",
"description": "MCP-first control plane for ProAgentStore agents and private instances.",
- "version": "0.1.6",
+ "version": "0.1.7",
"repository": {
"url": "https://github.com/ProAgentStore/platform",
"source": "github"
diff --git a/store/.well-known/mcp-server.json b/store/.well-known/mcp-server.json
index a515c05b..8ed07e48 100644
--- a/store/.well-known/mcp-server.json
+++ b/store/.well-known/mcp-server.json
@@ -3,7 +3,7 @@
"name": "io.github.ProAgentStore/platform",
"title": "ProAgentStore",
"description": "MCP-first control plane for ProAgentStore agents and private instances.",
- "version": "0.1.6",
+ "version": "0.1.7",
"repository": {
"url": "https://github.com/ProAgentStore/platform",
"source": "github"
diff --git a/store/about/index.html b/store/about/index.html
index feee5a75..1c9eb34c 100644
--- a/store/about/index.html
+++ b/store/about/index.html
@@ -176,7 +176,7 @@
Data isolation
Developer tools
CLI — npm i -g @proagentstore/cli then pags up: one runner serves ALL your agents over a WebSocket relay — no tunnel binary, no config. Plus pags login, pags init, pags check, pags publish.
- MCP Server — manage agents from Claude Code, Cursor, or VS Code. 136 tools across creator operations, public trials, private instance runtime, coding, and error observability (list_errors).
+ MCP Server — manage agents from Claude Code, Cursor, or VS Code. 137 tools across creator operations, public trials, private instance runtime, coding, and error observability (list_errors).
Skills — install the same ProAgentStore MCP workflow in Codex and Claude Code from the skills catalog.
Webhook ingestion — Zapier, Make, n8n can POST documents directly into an instance's knowledge base.
API key vault — store your OpenAI, Anthropic, Google AI keys once. Encrypted AES-256-GCM. Agents access via proxy.
diff --git a/store/llms-full.txt b/store/llms-full.txt
index 9c6e7f50..663087d9 100644
--- a/store/llms-full.txt
+++ b/store/llms-full.txt
@@ -156,7 +156,7 @@ That response means the private instance path is working and correctly refusing
## Main MCP Tool Groups
-The server registers 136 tools; 117 are always present and 19 are gated to the `apply`, `repo`, and `coding` surfaces. The lists below are the commonly used subset, not the full surface — call `tools/list` for the authoritative set, and see the tool table in `workers/mcp/README.md` for scopes, `dry_run` support, and confirmation values.
+The server registers 137 tools; 118 are always present and 19 are gated to the `apply`, `repo`, and `coding` surfaces. The lists below are the commonly used subset, not the full surface — call `tools/list` for the authoritative set, and see the tool table in `workers/mcp/README.md` for scopes, `dry_run` support, and confirmation values.
Creator tools:
diff --git a/store/openapi.yaml b/store/openapi.yaml
index b4884870..d7ed8b22 100644
--- a/store/openapi.yaml
+++ b/store/openapi.yaml
@@ -1068,6 +1068,49 @@ paths:
schema:
$ref: "#/components/schemas/Error"
+ /v1/auth/me/account:
+ get:
+ summary: Get current account identity
+ description: >
+ Which account this session is connected as — id, login, sign-in provider
+ (github/google) and label, email (surfaced only for Google, whose sign-in
+ stores the address; a GitHub login is a username, returned as `login`), roles,
+ account createdAt, and this token's tokenExpiry. Narrower than GET /v1/auth/me
+ (which is the profile shape); exists so an MCP `whoami` can name the identity.
+ Gated by the session Bearer only — the row returned is always the caller's own.
+ operationId: getMeAccount
+ tags: [Auth]
+ responses:
+ "200":
+ description: Account identity
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id: { type: string }
+ login: { type: string }
+ provider: { type: string, enum: [github, google] }
+ providerLabel: { type: string }
+ email: { type: string, nullable: true }
+ roles:
+ type: array
+ items: { type: string }
+ createdAt: { type: string }
+ tokenExpiry: { type: string, format: date-time }
+ "401":
+ description: Not authenticated or token expired
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ "404":
+ description: User not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+
# ── Agents ────────────────────────────────────────────────────────────────
/v1/agents/my/agents:
diff --git a/workers/api/src/routes/auth.integration.test.ts b/workers/api/src/routes/auth.integration.test.ts
index 52ed5273..ea2aaf9c 100644
--- a/workers/api/src/routes/auth.integration.test.ts
+++ b/workers/api/src/routes/auth.integration.test.ts
@@ -46,6 +46,17 @@ function buildApp(opts: { seedRoles?: string; env?: Partial } = {}) {
const u = users.find((x) => x.id === args[0]);
return u ? { roles: u.roles ?? (opts.seedRoles ?? null), github_login: u.github_login } : null;
}
+ // The /me/account identity read — distinguished from /me by selecting created_at.
+ if (sql.includes("created_at") && sql.includes("SELECT id, github_login")) {
+ const u = users.find((x) => x.id === args[0]);
+ if (!u) return null;
+ return {
+ id: u.id,
+ github_login: u.github_login,
+ roles: u.roles ?? '["user"]',
+ created_at: "2026-01-01T00:00:00Z",
+ };
+ }
if (sql.startsWith("SELECT id, github_login")) {
const u = users.find((x) => x.id === args[0]);
if (!u) return null;
@@ -397,3 +408,51 @@ describe("GET /v1/auth/me + PUT /v1/auth/me (integration)", () => {
expect(upd!.args).toContain("https://me.dev");
});
});
+
+describe("GET /v1/auth/me/account (integration)", () => {
+ const tokenFor = (uid: string, roles: string[] = ["user"]) => signSession(uid, SECRET, { roles });
+
+ it("401s without a bearer token", async () => {
+ const { app, env } = buildApp();
+ const res = await app.request("/v1/auth/me/account", {}, env);
+ expect(res.status).toBe(401);
+ });
+
+ it("404s a token whose user row doesn't exist", async () => {
+ const { app, env } = buildApp();
+ const res = await app.request("/v1/auth/me/account", { headers: { Authorization: `Bearer ${await tokenFor("ghost")}` } }, env);
+ expect(res.status).toBe(404);
+ });
+
+ it("derives the github provider + surfaces no email for a bare-numeric uid", async () => {
+ const { app, env, users } = buildApp();
+ users.push({ id: "12345", github_login: "octo", github_name: "Octo", avatar_url: "https://a", roles: '["user","creator"]' });
+ const token = await tokenFor("12345", ["user", "creator"]);
+ const res = await app.request("/v1/auth/me/account", { headers: { Authorization: `Bearer ${token}` } }, env);
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as Record;
+ expect(body.id).toBe("12345");
+ expect(body.login).toBe("octo");
+ expect(body.provider).toBe("github");
+ expect(body.providerLabel).toBe("GitHub");
+ // A GitHub login is a username, not an address — never surfaced as email.
+ expect(body.email).toBeNull();
+ expect(body.roles).toEqual(["user", "creator"]);
+ expect(body.createdAt).toBe("2026-01-01T00:00:00Z");
+ // tokenExpiry is the session's own exp, rendered ISO — parseable and in the future.
+ expect(Number.isNaN(Date.parse(body.tokenExpiry as string))).toBe(false);
+ expect(Date.parse(body.tokenExpiry as string)).toBeGreaterThan(Date.now());
+ });
+
+ it("derives the google provider + surfaces the stored email for a google: uid", async () => {
+ const { app, env, users } = buildApp();
+ users.push({ id: "google:99", github_login: "user@example.com", github_name: "User", avatar_url: "https://a", roles: '["user"]' });
+ const res = await app.request("/v1/auth/me/account", { headers: { Authorization: `Bearer ${await tokenFor("google:99")}` } }, env);
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as Record;
+ expect(body.provider).toBe("google");
+ expect(body.providerLabel).toBe("Google");
+ expect(body.email).toBe("user@example.com");
+ expect(body.login).toBe("user@example.com");
+ });
+});
diff --git a/workers/api/src/routes/auth.ts b/workers/api/src/routes/auth.ts
index 2d4fe014..51948d1d 100644
--- a/workers/api/src/routes/auth.ts
+++ b/workers/api/src/routes/auth.ts
@@ -569,3 +569,45 @@ authRoutes.get("/me", async (c) => {
boardConfig: parseJsonOrNull(row.board_config),
});
});
+
+/**
+ * GET /v1/auth/me/account — "which account am I connected as?"
+ *
+ * The gap this closes (mirrors ProAppStore #136): over the MCP connector a caller holds
+ * a session but has no tool that names the identity behind it. `/me` above is the SDK-ish
+ * profile shape (name/avatar/subscription/board); this is the narrower identity answer —
+ * who, via which provider, since when, and how long this token is still good for.
+ *
+ * Plain `requireUser` is the only gate. Unlike ProAppStore, PAGS does NOT de-privilege an
+ * MCP-origin session to `['user']` and does NOT ship this shape into creator-controlled app
+ * JS (there is no SDK `User` contract carrying it), so the internal-token second gate PAS
+ * needed here would be net-new plumbing guarding a leak PAGS does not have. The row returned
+ * is always the caller's OWN — there is no lookup-by-id parameter to abuse.
+ *
+ * `provider` is derived from the uid shape the OAuth callbacks mint: Google uids are
+ * `google:` (see /google/callback), GitHub uids are the bare numeric id. `email` is only
+ * meaningful for Google, whose callback stores the address in `github_login`; a GitHub
+ * `github_login` is a username, not an address, so it is surfaced as `login`, never `email`.
+ */
+authRoutes.get("/me/account", async (c) => {
+ const session = await requireUser(c);
+ const row = await c.env.DB.prepare(
+ "SELECT id, github_login, roles, created_at FROM users WHERE id = ?1",
+ )
+ .bind(session.uid)
+ .first<{ id: string; github_login: string; roles: string; created_at: string }>();
+ if (!row) return c.json({ error: "User not found" }, 404);
+
+ const isGoogle = session.uid.startsWith("google:");
+ const roles = JSON.parse(row.roles || '["user"]') as string[];
+ return c.json({
+ id: row.id,
+ login: row.github_login,
+ provider: isGoogle ? "google" : "github",
+ providerLabel: isGoogle ? "Google" : "GitHub",
+ email: isGoogle ? row.github_login : null,
+ roles,
+ createdAt: row.created_at,
+ tokenExpiry: new Date(session.exp * 1000).toISOString(),
+ });
+});
diff --git a/workers/mcp/AGENTS.md b/workers/mcp/AGENTS.md
index dbfc4664..4bf51e00 100644
--- a/workers/mcp/AGENTS.md
+++ b/workers/mcp/AGENTS.md
@@ -12,7 +12,7 @@ Connection methods and the full tool table: [`README.md`](./README.md).
unless the user explicitly overrides this.
2. **Discover the surface before using it.** The tool list is versioned and
- *per-connection*: of the 136 tool registrations, 19 are gated to the console surfaces
+ *per-connection*: of the 137 tool registrations, 19 are gated to the console surfaces
of the agents the connected user actually subscribes to (`apply`, `repo`, `coding`). A
tool you used last week may be absent today, and a tool present for one user is absent
for another. Call `tools/list` first; never assume a name exists.
diff --git a/workers/mcp/CLAUDE.md b/workers/mcp/CLAUDE.md
index 82e03c6d..4919f260 100644
--- a/workers/mcp/CLAUDE.md
+++ b/workers/mcp/CLAUDE.md
@@ -71,7 +71,7 @@ src/
├── settings.ts 11 tools — settings, name, instructions, model, translation, state
├── triggers.ts 5 tools — webhook / cron / connector-sync triggers
├── composition.ts 8 tools — supervision (#183), connections (#182), loops
- ├── account.ts 7 tools — billing, usage, keys, email, profile, budget limits
+ ├── account.ts 8 tools — whoami, billing, usage, keys, email, profile, budget limits
├── connectors.ts 4 tools — connector status and folder grants
├── stats.ts 4 tools — declarative stats cards (creator schema + subscriber override)
│ ── surface-gated ──
@@ -80,18 +80,18 @@ src/
└── coding.ts system_status + coding_timeline (gated to surfaces:["coding"]) + 3 loop tools
```
-**136 tool registrations** (`.tool(` in the files above): 31 in `index.ts` — 10 of them
-inside a `groups.has("coding")` block — 13 in `storage-tools.ts`, and 92 across
-`instance-tools/`. 117 are always registered; 19 are surface-gated (apply=4, repo=3, coding=12).
+**137 tool registrations** (`.tool(` in the files above): 31 in `index.ts` — 10 of them
+inside a `groups.has("coding")` block — 13 in `storage-tools.ts`, and 93 across
+`instance-tools/`. 118 are always registered; 19 are surface-gated (apply=4, repo=3, coding=12).
Those three numbers ADD UP to the headline, and that is the point of stating them: 31 + 13
-+ 92 = 136. They said 88 until #602, which made the paragraph sum to 132 — a total the
++ 93 = 137. They said 88 until #602, which made the paragraph sum to 132 — a total the
same sentence contradicted two clauses earlier. The per-file rows in the tree above are
machine-checked against `.tool(` counts; this prose sum is not, so it is the half that rots.
`base.ts` was 1871 lines and 67 of the 86 instance tools THEN REGISTERED until #305 — the
file a tool landed in when nobody decided where it went, and the largest in the repo. That
-86 is history and is not the current count (92); it is kept because it is what makes the
+86 is history and is not the current count (93); it is kept because it is what makes the
1871 lines legible. The nine ungated groups above are that file split along the
registration boundaries it already had; the blocks moved verbatim.
@@ -100,7 +100,7 @@ Tests sit beside their modules: `index.test.ts`, `index-auth.test.ts`,
`repo-tools.test.ts`, `safety.test.ts`, `storage-tools.test.ts`.
`instance-tools/contract.test.ts` is the one to know about. It holds every tool registered
-under `instance-tools/` — 92 of them — to a table of **group, scope, confirmation string,
+under `instance-tools/` — 93 of them — to a table of **group, scope, confirmation string,
dry-run behaviour and input fields** —
and every value in that table is DERIVED by driving the registered handler (call it holding
only `read`, then holding everything but `read`, and read the required scope out of the
diff --git a/workers/mcp/README.md b/workers/mcp/README.md
index c2428090..b3bf807c 100644
--- a/workers/mcp/README.md
+++ b/workers/mcp/README.md
@@ -126,7 +126,7 @@ implementation.
## Tools
-**136 tool registrations.** 117 are always registered; 19 are gated to the console
+**137 tool registrations.** 118 are always registered; 19 are gated to the console
surfaces of the connected user's subscribed agents (`apply`, `repo`, `coding`), so a
Repo Chat user never sees `apply_to_job`.
@@ -367,6 +367,7 @@ immediately instead of a whole transcript.
| `list_pipeline_runs` | Declarative-pipeline runs with counts | — | | |
| `get_instance_pipeline` | Read back a single stored pipeline definition (with validity) | — | | |
| `mcp_audit_log` | Recent MCP write/runtime/dry-run/denied events for this account | read | | |
+| `whoami` | Which account you are connected as — id, login, sign-in provider, email (Google only), roles, createdAt, token expiry | — | | |
| `billing_status` | Free vs Pro, paywall enforcement | — | | |
| `usage_summary` | Token usage + estimated cost by agent/model/activity | — | | |
| `keys_status` | Which providers have a BYOK key — **names only** | — | | |
diff --git a/workers/mcp/src/instance-tools/account.ts b/workers/mcp/src/instance-tools/account.ts
index f221aae2..8b4ad006 100644
--- a/workers/mcp/src/instance-tools/account.ts
+++ b/workers/mcp/src/instance-tools/account.ts
@@ -15,6 +15,20 @@ import type { InstanceToolsCtx } from "./shared.js";
export function registerAccountTools(server: McpServer, ctx: InstanceToolsCtx): void {
const { env, tokenFor, safetyFor } = ctx;
+ server.tool(
+ "whoami",
+ "Which account this connection is signed in as: id, login, sign-in provider — the string `github` or `google` — plus a display label, email (only when signed in with Google — a GitHub login is a username, not an address, and comes back as `login`), roles, account createdAt, and this token's tokenExpiry. Answers 'who am I connected as?' — nothing here is a secret. For plan/billing use billing_status; for BYOK keys use keys_status.",
+ {
+ token: z.string().optional().describe("PAGS session token. Omit when connected with browser sign-in."),
+ },
+ async ({ token }) => {
+ const sessionToken = tokenFor(token);
+ if (!sessionToken) return authRequired();
+ const data = await authedCall("/v1/auth/me/account", sessionToken, {}, env);
+ return jsonText(data);
+ },
+ );
+
server.tool(
"billing_status",
"Read your billing/plan status (free vs Pro, whether the paywall is enforced, whether a billing account exists). Upgrades happen in the console (browser redirect).",
diff --git a/workers/mcp/src/instance-tools/contract.test.ts b/workers/mcp/src/instance-tools/contract.test.ts
index 96626898..e7efa996 100644
--- a/workers/mcp/src/instance-tools/contract.test.ts
+++ b/workers/mcp/src/instance-tools/contract.test.ts
@@ -316,6 +316,7 @@ const TABLE: Record = {
// 64 KiB limit, so the inventory is paged. Still ungated — paging a read changes nothing
// about what it may see.
vector_stats: ["knowledge", "none", null, null, "instance_id,limit,offset,token"],
+ whoami: ["account", "none", null, null, "token"],
write_instance_memory: ["knowledge", "write", null, "envelope", "content,dry_run,instance_id,key,token,type"],
};
diff --git a/workers/mcp/src/server-version.ts b/workers/mcp/src/server-version.ts
index c05a419f..cf9958b6 100644
--- a/workers/mcp/src/server-version.ts
+++ b/workers/mcp/src/server-version.ts
@@ -88,4 +88,4 @@
*/
/** Advertised in `serverInfo.version`, and restated in `server.json` and `platform-docs/mcp.md`. */
-export const MCP_SERVER_VERSION = "0.1.6";
+export const MCP_SERVER_VERSION = "0.1.7";
diff --git a/workers/mcp/src/surface-lock.ts b/workers/mcp/src/surface-lock.ts
index 4f55bad2..5926619f 100644
--- a/workers/mcp/src/surface-lock.ts
+++ b/workers/mcp/src/surface-lock.ts
@@ -116,4 +116,9 @@ export const SURFACE_LOCK: Record = {
// through when `my_instances` became `{"instances":[…]}` under a frozen version, so the bump
// is justified twice over here as it was for 0.1.5. Appended, never edited: 0.1.5 is published.
"0.1.6": "sha256:bdf6eb2efd98c4df362bbc6762537ca8b687dc9a03ce21a519326a73c21b9e98",
+ // 0.1.7 (#672): `whoami` registered — a new tool NAME in the always-on `account` group,
+ // the first bullet of `MCP_SERVER_VERSION`'s list, so the served surface grew by one and
+ // this hash moves. It reads the new `GET /v1/auth/me/account` and is annotated `read`
+ // (`readOnlyHint: true`), matching its ungated gate. Appended, never edited: 0.1.6 is published.
+ "0.1.7": "sha256:5a0bca0af76e29fa7ac6dcd673d76a8ba7a32dc9c6d506f3aa7b2e30c99eade8",
};
diff --git a/workers/mcp/src/tool-count.ts b/workers/mcp/src/tool-count.ts
index fc7f255d..743bfc79 100644
--- a/workers/mcp/src/tool-count.ts
+++ b/workers/mcp/src/tool-count.ts
@@ -20,10 +20,10 @@
*/
/** Every tool the server can register, with all surfaces gated on. */
-export const MCP_TOOL_COUNT = 136;
+export const MCP_TOOL_COUNT = 137;
/** Registered for every connection, whatever the user is subscribed to. */
-export const MCP_TOOL_ALWAYS_ON = 117;
+export const MCP_TOOL_ALWAYS_ON = 118;
/** Registered only when the user has an agent with the matching console surface
* (`apply`, `repo`, `coding`) — so a Repo Chat user never sees `apply_to_job`. */
diff --git a/workers/mcp/src/tool-metadata.ts b/workers/mcp/src/tool-metadata.ts
index d122ee9a..d406268d 100644
--- a/workers/mcp/src/tool-metadata.ts
+++ b/workers/mcp/src/tool-metadata.ts
@@ -167,6 +167,7 @@ export const TOOL_RISK: Record = {
ticket_thread: "read",
usage_summary: "read",
vector_stats: "read",
+ whoami: "read",
// ── write: adds to or updates PAGS state, and this server grants it on a default
// connection. Announced `readOnlyHint: false, destructiveHint: false`. ──
@@ -270,7 +271,7 @@ export const TOOL_RISK: Record = {
/** How the surface splits. A ratchet in BOTH directions: silently losing a read-only
* annotation is as much a regression as silently gaining one. */
export const MCP_RISK_COUNTS: Record = {
- read: 67,
+ read: 68,
write: 40,
runtime: 15,
destructive: 14,