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

A test pins who may call an MCP tool: `visible` hides admin tools, and `plansOnly` lets a read-only token only read or plan #6662

Merged opened this

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

Merged

This adds `test/mcp-shared-gate.test.ts`, which pins who may see and call an MCP tool. It has 13 cases and all pass. `bun x tsc --noEmit` is clean. I made no source changes because no case failed.

shipped production is serving it deploy 0af2c9c

opened this

This adds test/mcp-shared-gate.test.ts, which pins who may see and call an MCP tool. It has 13 cases and all pass. bun x tsc --noEmit is clean. I made no source changes because no case failed.

  • visible:

- An adminOnly tool is hidden from a non-admin and shown to an admin. - A needsAdminScope tool is hidden on ro and rw tokens and shown on an admin one, whoever the user is. - A tool with both flags needs both. - An unset scope counts as ro.

  • plansOnly:

- A dryByDefault tool plans unless dryRun is exactly false; the string "false" still plans. - A readsWithout: "x" tool reads only while x is absent, and the array form needs every key absent. - readsWhen is honoured. - A plain write tool is not treated as a plan.

  • lean: it strips maxLength, minLength, maxItems and minItems at every depth, including inside anyOf. Everything else, such as enum, required and minimum, survives.

I built the Ctx and User as bare objects cast to the types. Each carries only the fields the gate reads (auth.scope and isAdmin), so no server or database starts.

To try it, run bun test test/mcp-shared-gate.test.ts.

Closes #6660

commented

This adds test/mcp-shared-gate.test.ts, which pins who may see and call an MCP tool. It has 13 cases and all pass. bun x tsc --noEmit is clean. I made no source changes because no case failed.

  • visible:

- An adminOnly tool is hidden from a non-admin and shown to an admin. - A needsAdminScope tool is hidden on ro and rw tokens and shown on an admin one, whoever the user is. - A tool with both flags needs both. - An unset scope counts as ro.

  • plansOnly:

- A dryByDefault tool plans unless dryRun is exactly false; the string "false" still plans. - A readsWithout: "x" tool reads only while x is absent, and the array form needs every key absent. - readsWhen is honoured. - A plain write tool is not treated as a plan.

  • lean: it strips maxLength, minLength, maxItems and minItems at every depth, including inside anyOf. Everything else, such as enum, required and minimum, survives.

I built the Ctx and User as bare objects cast to the types. Each carries only the fields the gate reads (auth.scope and isAdmin), so no server or database starts.

To try it, run bun test test/mcp-shared-gate.test.ts.

Approved

Approved. The test-only change matches the source: the cases follow visible, plansOnly and lean in src/http/mcp/shared.ts line by line, and the file passes locally. It adds no dependency, boots no server and touches no source. Each case would fail if the gate were loosened: adminOnly needs user.isAdmin, needsAdminScope needs an admin-scope token whoever the user is, and an unset scope counts as ro. Two things to do next: (1) add one case through the real dispatch path, so a ro token calling a write tool is shown to be refused and not just classified by plansOnly (test/mcp-write-scope.test.ts may already cover it, so check that first); (2) in plansOnly, a dryRun of the string "false" still plans. That is the safe side. Say it in a comment or in the tool schema so callers are not surprised.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs