Skip to content

feat(mcp): run MCP on the Platform kit, ready for the Claude and ChatGPT directories - #174

Merged
clbotdev merged 6 commits into
mainfrom
t3/mcp-server-kit
Nov 1, 2026
Merged

clbotdev merged 6 commits into
mainfrom
t3/mcp-server-kit

Conversation

@clbotdev

@clbotdev clbotdev commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Re-opens #173 against main. #173 merged into t3/login-turnstile seconds after #172 merged, before GitHub retargeted it, so its commits never reached main. Same commits, unchanged and already reviewed; the description below is #173's.

What

/mcp runs on @codelitdev/mcp-server-kit instead of SendLit's own MCP SDK server, wiring and tool registry (ADR 0010).

What SendLit keeps:

  • Its own credential and team resolution: resolveAuth, and resolveUserTeam, which is now shared with REST's requireTeam.
  • x-sendlit-apikey, beside the kit's X-API-Key.
  • The OAuth discovery routes and the WWW-Authenticate challenge on 401.
  • Per-tool rate limits and request logs.

Tools: each of the 115 tools still maps to one REST operation (111 operations plus 4 ESP aliases), with the same names and schemas. dedicatedTool() builds each one from its REST operation id. policy.ts maps every MCP-reachable operation to its OAuth scope, and team keys still can't manage API keys.

What the Claude and ChatGPT directories review is enforced and tested:

  • every tool has a title and a name of at most 64 characters
  • every tool advertises readOnlyHint, destructiveHint, openWorldHint and idempotentHint as explicit booleans
  • destructive tools advertise confirm, which the kit requires to be true

Errors: tool failures are platform errors (not_found, conflict, validation_failed, forbidden, …). details.reason carries SendLit's own code (for example plan_limit_reached or template_not_transactional) and details.message explains it. Unexpected errors are reported and returned as internal_error, never with their raw message. Input validation errors name the failing field.

Fixed along the way: get_plan_usage looked the team up by the wrong id, so it always returned not-found.

Platform: every @codelitdev/* package moves to 0.8.0. 0.7.0 added tool titles and output schemas (codelitdev/platform#22) and 0.8.0 explicit hints (codelitdev/platform#24). Managed files are synced.

History in this branch

The first commit tried CourseLit's generic api.* tools (discovery plus one generic executor per risk). Before release I removed them again: OpenAI's plugin guidelines reject "discovery, operation selection, or schema fetching with a generic executor". ADR 0010 records why. Squash-merge if you'd rather not keep that detour in history.

Breaking for MCP clients

The server version moves to 3.0.0:

  • tool names are unchanged
  • destructive tools now need confirm: true
  • error results have the platform shape instead of free text

Testing

  • 1009 API tests; conformance, parity and drift; lint and typecheck; platform-cli doctor.
  • Live stack, team API key (2026 and 2025 protocol eras): 115 tools, all with a title and explicit hints. delete_contact refuses without confirm and deletes with it. Team keys are refused key management. A request without credentials gets 401 with the OAuth challenge.
  • Browser end-to-end over the tailnet stack, using Turnstile test keys (headless run plus a recorded run in the T3 preview):
    • dashboard sign-in through Turnstile and the emailed code; Contacts, Sequences and Settings load
    • MCP OAuth: dynamic client registration and PKCE → /oauth/login with Turnstile → consent shows contacts:read sequences:read → code → token with exactly those scopes
    • with that token: list_contacts and list_sequences work, send_email is refused with insufficient_scope, and an invalid token gets 401 with the challenge

….* tools

SendLit's own SDK 2 server registered one tool per REST operation (118).
/mcp now runs on @codelitdev/mcp-server-kit, following CourseLit: 29
dedicated tools for the everyday flows (contacts, segments, templates,
sequences and broadcasts, transactional email, events, delivery events, plan
usage), and api.list_operations / api.describe_operation / api.read /
api.write / api.delete for every other team operation in the parity
manifest. Generic calls are real REST requests to the same app over a
loopback listener, with the caller's credential, team, IP and request id.

Authorization is per operation (policy.ts maps each MCP-reachable operation
to its OAuth scope) for both kinds of tool; team keys still can't manage keys.
The route keeps SendLit's auth and team resolution (now shared with REST via
resolveUserTeam), x-sendlit-apikey, the OAuth discovery routes, the
WWW-Authenticate challenge and per-tool rate limits. Tool errors are platform
errors whose details keep SendLit's reason and message.

Breaking for MCP clients: removed tools are reached through api.* and
deletes need confirm: true; server version 3.0.0. See ADR 0010.

Needs @codelitdev/mcp-server-kit with tool title/outputSchema/annotations
(codelitdev/platform#22).
0.7.0's mcp-server-kit carries tool titles, output schemas and the
idempotent/open-world hints the MCP tools use.
…y review

Drop the generic api.* tools: the ChatGPT Plugins Directory requires each
model-callable operation to be its own tool and rejects discovery plus a
generic executor. All 115 tools are back on the kit, built by
dedicatedTool() from their REST operation, with their former names and
schemas.

For directory review, every tool now has a title and an explicit
openWorldHint, destructive tools advertise confirm (which the kit requires),
and tests pin these. Unexpected errors no longer echo raw messages; known
ones keep SendLit's reason. The generic tools' loopback listener, replayed
credentials and client IP are gone. ADR 0010 records the decision.
0.8.0's mcp-server-kit advertises every tool hint as an explicit boolean, as
the ChatGPT Plugins Directory requires.
@clbotdev
clbotdev merged commit 2ed07cb into main Nov 1, 2026
5 of 8 checks passed
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.

2 participants