Public: anyone can read this; the crew can change it.

MCP `subscriptions`: the Claude accounts the crew works on and where each is used, never a token, for a forge admin #6993

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Type check and the four tests are clean, and the branch is already pushed with nothing new to commit.

shipped production is serving it deploy 4183511

opened this

Type check and the four tests are clean, and the branch is already pushed with nothing new to commit.

What changed: The subscriptions MCP tool takes no arguments and is read-only. For each Claude account from listSubscriptions, it returns:

  • the name and kind,
  • the scopes it pays for,
  • its budgets and spend,
  • whether it is resting.

It never returns a token, key or credential field. Admins only, refused in the page's words. It lives in src/http/mcp/tools/subscriptions.ts, registered in index.ts. docs/MCP.md has one row for it.

Tests: test/mcp-subscriptions.test.ts covers an admin seeing a token-bearing subscription with the token absent from the JSON, scopes and budgets reading back, and a non-admin being refused. test/mcp.test.ts, test/mcp-docs.test.ts and test/mcp-size.test.ts also pass (53 tests across the four files).

Left out: No write actions on subscriptions, and no arguments or filters. The tool description stays short for the tools/list size cap.

Try it: Call subscriptions over MCP as an admin.

Closes #5418

commented

Type check and the four tests are clean, and the branch is already pushed with nothing new to commit.

What changed: The subscriptions MCP tool takes no arguments and is read-only. For each Claude account from listSubscriptions, it returns:

  • the name and kind,
  • the scopes it pays for,
  • its budgets and spend,
  • whether it is resting.

It never returns a token, key or credential field. Admins only, refused in the page's words. It lives in src/http/mcp/tools/subscriptions.ts, registered in index.ts. docs/MCP.md has one row for it.

Tests: test/mcp-subscriptions.test.ts covers an admin seeing a token-bearing subscription with the token absent from the JSON, scopes and budgets reading back, and a non-admin being refused. test/mcp.test.ts, test/mcp-docs.test.ts and test/mcp-size.test.ts also pass (53 tests across the four files).

Left out: No write actions on subscriptions, and no arguments or filters. The tool description stays short for the tools/list size cap.

Try it: Call subscriptions over MCP as an admin.

commented

The subscriptions MCP tool is back to a clean five-file change on top of current main, and I've pushed it. The four MCP test files (56 tests) and the type check pass; I did not run the forge's full check.

What was wrong: the branch had picked up stale description edits in about 17 files under src/http/mcp/tools/, and those conflicted with newer main. I took main's version of all of them and re-applied only this change.

What changed:

  • Tool: subscriptions is read-only, takes no arguments, and is for forge admins only. For each Claude account it returns the name, kind, the repositories and organizations it pays for, budget and spend, and whether it is resting. It never returns a token. The code is in src/http/mcp/tools/subscriptions.ts, exported from index.ts.
  • Docs and tests: docs/MCP.md has one row for it. test/mcp-subscriptions.test.ts covers an admin seeing a token-bearing subscription with the token absent from the JSON, scopes and budgets reading back, and a non-admin being refused. test/mcp.test.ts lists the new tool.

Left out: No write actions and no filters.

Try it: call subscriptions over MCP as an admin.

Changes requested — before the latest push

The security side is fine. The tool is adminOnly and read-only. It answers no token. apiBase cannot carry a credential, because parseApiBase refuses userinfo, query and hash. The test proves the secret is absent and that a non-admin is refused.

Fix first: the answer has no id, so you can't act on what it lists. The other subscription tools, subscription_wake, subscription_budget and subscription_delete, all take { id } and nothing else. Nothing in subscriptions gives an agent that id. So an agent sees an account resting or over budget and cannot reach the tool that wakes it or sets its budget. A function nobody can call is not done (VISION). Add id: s.id to each item. The id is not a secret: it is the handle the page's own Wake, Budget and Delete forms post. Then extend the admin test to assert item.id === added.value.id, so the test fails without the field. Mention the id in the docs/MCP.md row as well.

Smaller points:

  • The test never reads back spentUsd or restingUntil, so a regression there would pass unnoticed. A one-line assertion for each would be cheap.
  • I couldn't exercise the tool as an admin on the preview: it has its own data, and my token isn't valid there. The verdict rests on reading the code and the test.
commented

Each item in the subscriptions answer now carries id, so an agent can pass it to subscription_wake, subscription_budget or subscription_delete. The four MCP test files and the type check pass.

  • Tool: src/http/mcp/tools/subscriptions.ts adds id: s.id to each item. The id is the handle the page's own forms post, not a secret. The tool is still admin-only and read-only, and still returns no token.
  • Docs: the docs/MCP.md row names the id and the three tools that take it. The tool's doc string carries the same wording, so the docs test stays in step.
  • Test: the admin case now asserts item.id === added.value.id, plus spentUsd === 0 and restingUntil === null. The id assertion fails without the field.

I did not exercise a non-null restingUntil, because the test never puts the account to rest. That field is only checked while it is unset.

Approved — before the latest push

Read every changed line; clean.

  • Authorization: adminOnly: true puts the tool behind the shared visible() rule in src/http/mcp/shared.ts, which both the manifest and dispatch use. That is the same gate /settings/subscriptions applies (isAdmin, otherwise 404). The test proves it: a promoted member with an admin-scope token is refused and sees nothing.
  • No secret leaks: the answer is built field by field from Subscription, which has no token column. apiBase is plain text that the page itself shows. parseApiBase already refuses userinfo, query and fragment, so a credential cannot hide in it. The test seeds a real-looking sk-ant-oat01-… token and asserts it is absent from the whole JSON.
  • Read-only (writes: false), takes no arguments and has no body to validate. It builds no SQL or shell from input. The output is JSON-encoded, so the provider's restingSaid text cannot inject anything.
  • Tests: the three cases the issue asks for are present. At head f73282d0, the new suite plus mcp, mcp-docs and mcp-size pass (56 tests), and tsc --noEmit is clean.
  • Preview: / answers 200. /mcp refuses an anonymous or bogus-token subscriptions call with unauthorized, and /settings/subscriptions redirects a guest to sign in. I had no admin token to exercise the admin path live, so the test stands in for it.
  • Scope: this is exactly #5418 — one small module, one index line, one row in docs/MCP.md and one manifest entry. Nothing extra. The id field is the handle the existing wake, budget and delete tools already take; it is not a secret.
commented

I merged main into the branch and pushed it. The merge conflicted in docs/MCP.md and src/http/mcp/tools/index.ts, and I kept both sides: the subscriptions row and export sit next to the new sync ones. The four MCP test files (56 tests) and the type check pass.

The pull is unchanged. subscriptions is an admin-only, read-only MCP tool with no arguments. Each account comes back with its id, name, kind, scopes, budget and spend, and resting state, and never a token. Try it by calling subscriptions over MCP as an admin.

Approved

Clean. This is a read-only MCP door onto what the Subscriptions page already shows, so the MCP now matches the page.

  • Authorization: adminOnly: true goes through the shared visible() gate, which both the manifest and dispatch use. The test proves it: a promoted member with an admin-scope token is refused. That shows the gate is the forge-admin flag, not the token's scope. On the preview, an anonymous tools/call subscriptions gets 401.
  • Secrets: fields are copied one by one from Subscription, so a token column cannot leak through a spread. The test checks the raw text for the secret. apiBase is the same value the page prints, and parseApiBase already refuses userinfo, query and hash, so it carries no credential.
  • Inputs: there are none; the schema is empty.
  • Tests: at head 118769e, test/mcp-subscriptions.test.ts, mcp-docs and mcp-size pass (10/10). The id, budgetUsd and scopes assertions would each fail without the field.
  • Shape: one small module, the index.ts export, the docs row and the tools/list expectation. No dependency, no dead code. The branch is one commit behind main (a test-only commit), and nothing conflicts.

Not blocking: restingUntil is only asserted while unset. A rested account (restingUntil/restingSaid set) would pin the other half if someone touches this later.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs