Skip to content

feat: add MCP subscribe and unsubscribe endpoints - #1183

Open
alexluong wants to merge 7 commits into
feat/mcp-schemasfrom
feat/mcp-subscribe-endpoints
Open

alexluong wants to merge 7 commits into
feat/mcp-schemasfrom
feat/mcp-subscribe-endpoints

Conversation

@alexluong

@alexluong alexluong commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Add endpoints that serve the MCP Events (draft SEP-3415) events/subscribe and events/unsubscribe methods, so an operator's MCP server forwards them to Outpost in one call instead of managing mcp destinations itself. Experimental, behind EXPERIMENTAL_MCP_ENABLED.

Stacked on #1182. Deviations from the draft, including what these endpoints leave out: see #1181.

Why this approach

The protocol rules live in one tested place in Outpost: ID derivation, lifetime grants, refresh and expiry, error codes. The MCP server stays a thin forwarder that works in any language, and a subscribe is one Outpost call. The cost is that Outpost owns the MCP wire format and moves with the draft, which is acceptable while the feature is experimental.

Changes

  1. Subscribe. POST /tenants/{tenant_id}/mcp/subscribe with {"principal", "params", "allowed_topics"?}, where params are the MCP request params as received. It creates the mcp destination, running the same checks as destination create, and answers with the MCP result (id, refreshBefore, cursor: null, truncated: false).
  2. Idempotent subscriptions. The subscription id is derived from principal, callback URL, event name and arguments. Subscribing again with the same values refreshes it: same id, new expiry, rotated secret, re-enabled if it was suspended. The refresh result's deliveryStatus.active says whether it was active before the refresh. An expired subscription is replaced by a new one. Concurrent subscribes for the same subscription can't create duplicates (fix: make concurrent tenant and destination writes safe #1172).
  3. Lifetime. The requested ttlMs is clamped to EXPERIMENTAL_MCP_SUBSCRIPTION_TTL_MIN_SECONDS (300) and EXPERIMENTAL_MCP_SUBSCRIPTION_TTL_MAX_SECONDS (86400). No ttlMs gets EXPERIMENTAL_MCP_SUBSCRIPTION_TTL_DEFAULT_SECONDS (3600), and null gets the maximum, so every grant is finite, which the draft allows.
  4. Unsubscribe. POST /tenants/{tenant_id}/mcp/unsubscribe deletes the subscription identified by the same principal, event, arguments and callback URL (not found otherwise).
  5. Errors. Client-side failures return 422 with an mcp_error (code, message, and data only where the draft defines it) for the MCP server to return as its JSON-RPC error. data holds only the fields the draft defines; Outpost's own detail (field names, reasons, retryAfterMs) stays in logs and the REST 422 mapping. -32602 invalid params and -32024 forbidden carry no data. -32023 not found has data.kind, -32025 resource exhausted has data.limit (and data.max when there is one), -32026 unsupported has data.feature and data.value (a delivery mode other than webhook), -32027 callback endpoint error has data.reason (a fixed category, never endpoint output). A callback verification rate limit maps to -32025. A verification store outage is not a client error: it returns an internal error. A subscribe that keeps losing to concurrent changes of the same subscription, or whose tenant is deleted meanwhile, returns 503 with Retry-After; a retry succeeds.
  6. Allowed topics. allowed_topics restricts the events a principal may subscribe to: omitted allows all, an empty list allows none. A subscription to a refused event answers Forbidden; the check runs before the catalog lookup, so an unknown name outside the list is Forbidden too.

Both routes are Admin API key only and return 404 while MCP is off.

Behavior changes

  • Subscribe creates the tenant when it is missing, before the destination checks run. A subscribe that then fails (unknown event, failed verification, full tenant) leaves the empty tenant behind.
  • New config: EXPERIMENTAL_MCP_SUBSCRIPTION_TTL_DEFAULT_SECONDS, EXPERIMENTAL_MCP_SUBSCRIPTION_TTL_MIN_SECONDS, EXPERIMENTAL_MCP_SUBSCRIPTION_TTL_MAX_SECONDS. Startup fails unless min <= default <= max.

Not in this PR

  • A route to revoke all subscriptions of a principal: the destination list and delete APIs cover it.

🤖 Generated with Claude Code

@alexluong
alexluong force-pushed the feat/mcp-subscribe-endpoints branch from c374170 to 5cf245d Compare October 30, 2026 20:51

This branch has not been deployed

No deployments
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