Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/mcp-instance-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions platform-docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` —
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion store/.well-known/mcp-server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion store/about/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ <h2>Data isolation</h2>

<h2>Developer tools</h2>
<p><strong>CLI</strong> — <code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">npm i -g @proagentstore/cli</code> then <code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">pags up</code>: one runner serves ALL your agents over a WebSocket relay — no tunnel binary, no config. Plus <code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">pags login</code>, <code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">pags init</code>, <code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">pags check</code>, <code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">pags publish</code>.</p>
<p><strong>MCP Server</strong> — manage agents from Claude Code, Cursor, or VS Code. 136 tools across creator operations, public trials, private instance runtime, coding, and error observability (<code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">list_errors</code>).</p>
<p><strong>MCP Server</strong> — manage agents from Claude Code, Cursor, or VS Code. 137 tools across creator operations, public trials, private instance runtime, coding, and error observability (<code style="background:var(--panel);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.85rem">list_errors</code>).</p>
<p><strong>Skills</strong> — install the same ProAgentStore MCP workflow in Codex and Claude Code from <a href="/skills/">the skills catalog</a>.</p>
<p><strong>Webhook ingestion</strong> — Zapier, Make, n8n can POST documents directly into an instance's knowledge base.</p>
<p><strong>API key vault</strong> — store your OpenAI, Anthropic, Google AI keys once. Encrypted AES-256-GCM. Agents access via proxy.</p>
Expand Down
2 changes: 1 addition & 1 deletion store/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
43 changes: 43 additions & 0 deletions store/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
59 changes: 59 additions & 0 deletions workers/api/src/routes/auth.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ function buildApp(opts: { seedRoles?: string; env?: Partial<Env> } = {}) {
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;
Expand Down Expand Up @@ -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<string, unknown>;
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<string, unknown>;
expect(body.provider).toBe("google");
expect(body.providerLabel).toBe("Google");
expect(body.email).toBe("user@example.com");
expect(body.login).toBe("user@example.com");
});
});
42 changes: 42 additions & 0 deletions workers/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<id>` (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(),
});
});
2 changes: 1 addition & 1 deletion workers/mcp/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 7 additions & 7 deletions workers/mcp/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──
Expand All @@ -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.

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion workers/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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** | — | | |
Expand Down
14 changes: 14 additions & 0 deletions workers/mcp/src/instance-tools/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down
1 change: 1 addition & 0 deletions workers/mcp/src/instance-tools/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ const TABLE: Record<string, Row> = {
// 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"],
};

Expand Down
2 changes: 1 addition & 1 deletion workers/mcp/src/server-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading
Loading