Skip to content

Expose the publish API as a remote MCP endpoint at /mcp#30

Open
koomen wants to merge 1 commit into
mcp-oauthfrom
mcp-endpoint
Open

Expose the publish API as a remote MCP endpoint at /mcp#30
koomen wants to merge 1 commit into
mcp-oauthfrom
mcp-endpoint

Conversation

@koomen

@koomen koomen commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Second of two PRs (stacked on #29). Adds the /mcp endpoint itself: after this lands, connecting from Claude Code is

claude mcp add --transport http scratchwork https://app.<domain>/mcp

then /mcp to authenticate in the browser — no CLI install. See the new README section.

Design

  • Stateless streamable HTTP: every response is a single application/json message (spec-permitted alternative to SSE), no Mcp-Session-Id is ever issued, and every POST is self-contained — required by the Lambda/Workers targets where consecutive calls land on different instances. Hand-rolled over @effect/ai's McpServer, which is session-oriented and routes through HttpRouter; rationale recorded in the transport.ts header (invariant 1 judgment call — the stateless tools-only subset is five methods over Effect Schemas).
  • invokeEndpoint refactor (api-routes.ts): the endpoint pipeline — bearer requirement, project read gate with its existence mask, strict payload decode, registered handler — extracted from runRoute and shared. Behavior-preserving by construction (args is a lazy effect, so the HTTP path's exact ordering is unchanged; the untouched policy matrix is the regression gate). MCP tools reach handlers only through it, so tool authorization is the API_POLICY registry (invariant 4).
  • Seven tools at CLI-operation altitude: publish (takes {path, content|contentBase64} files — the agent reads local files itself; the server builds the bundle through the existing shared decode and publish caps), list_projects, project_info, share_project, unpublish_project, delete_project, whoami. tools/list input/output JSON Schemas are derived from the same Effect Schemas that validate calls.
  • Auth: bearer-only via the requireMcpUser chokepoint from Add MCP OAuth 2.1 surface: discovery, DCR, authorize/consent, token #29, after the standard cross-origin gate; unauthenticated calls get the WWW-Authenticate resource-metadata 401 that triggers a client's OAuth flow. Domain failures surface as isError tool results carrying exactly the masked messages the HTTP routes emit.
  • Transport body cap 34 MB (publish's 30 MB + envelope headroom); per-endpoint content caps run inside the handlers unchanged.

Tests

  • mcp.test.ts: framing (-32700/-32600/-32601/-32602, batch rejection, notifications → 202), version negotiation for both supported revisions, statelessness (tools/call with no prior initialize, no session header), 401/403/405/413 edges, per-tool round trips with served-byte verification, and a policy-parity matrix: every tool × credential × visibility cell derives its expected outcome from API_POLICY[tool.endpoint] — strangers get the same "Project not found" / "already taken" masks the HTTP routes send.
  • e2e: a full spec-client loop — 401 challenge → discovery → DCR → browser authorize/consent → token → refresh → stateless tool publishes (text + binary) verified byte-for-byte at the served URL — on all three lanes (local Bun, miniflare, LocalStack), since serverless statelessness is the claim under test.

bun run ci green.

🤖 Generated with Claude Code

Claude Code (or any MCP client) can now publish and manage Scratchwork
projects with no CLI installed: `claude mcp add --transport http
scratchwork <app-origin>/mcp`, authenticate via /mcp (the OAuth surface
from the previous commit), and use tools backed by the same contract
endpoints the CLI calls.

- api-routes.ts: extract invokeEndpoint — the transport-independent
  endpoint pipeline (bearer requirement, project read gate with its
  existence mask, strict payload decode, registered handler). runRoute
  delegates to it, byte-for-byte behavior-preserving, so MCP tools
  inherit route policy instead of reimplementing it (invariant 4).
- mcp/jsonrpc.ts + mcp/transport.ts: a deliberately stateless MCP
  streamable-HTTP transport — single JSON responses, no Mcp-Session-Id,
  every POST self-contained — because Lambda/Workers instances share no
  memory; hand-rolled over @effect/ai's session-oriented McpServer
  (rationale in the module header). Bearer-only via requireMcpUser after
  the standard cross-origin gate; the 401 carries the WWW-Authenticate
  resource-metadata pointer that starts a client's OAuth flow.
- mcp/tools.ts: seven tools at CLI-operation altitude (publish,
  list_projects, project_info, share_project, unpublish_project,
  delete_project, whoami), each wrapping one contract endpoint; publish
  takes {path, content|contentBase64} files and feeds the existing
  shared bundle decode and publish caps. tools/list schemas are derived
  from the same Effect Schemas that validate calls.
- Tests: framing/negotiation/statelessness suite plus a policy-parity
  matrix deriving every tool's expected outcome from API_POLICY; e2e
  gains a full spec-client loop (401 challenge → discovery → DCR →
  authorize/consent → token → refresh → stateless tool publishes with
  served-byte verification) on all three lanes — local Bun, miniflare,
  LocalStack — since serverless statelessness is the claim under test.
- README: "Connect from Claude Code" section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant