feat(plugins): allow explicit MCP tool declarations#2002
Conversation
🦋 Changeset detectedLatest commit: 11fcd44 The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 1,733 lines across 76 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
This is a sound, well-scoped feature that matches the approved Discussion. It introduces explicit, opt-in plugin MCP tools with the right separation of concerns: plugin declarations at build time, deterministic <pluginId>__<tool> names, admin consent in a dedicated DB column, plugin-scoped bearer tokens, RBAC permission enforcement inside the MCP server, and audit logging. The changeset, migration (052), docs, admin UI strings (Lingui), RTL-aware Tailwind, and test coverage (unit + site-matrix smoke) are all in place.
I checked authorization flow, scope validation, the plugin route permission change, migration/schema consistency, API envelope shape, and admin UI conventions. I found three issues worth fixing before merge:
-
handlePluginMcpToolreuses the incomingContent-Lengthheader for a new, shorter request body. The internalRequestcopiesrequest.headersverbatim while replacing the body withJSON.stringify(input). The originalContent-Lengthcorresponds to the much larger MCP JSON-RPC batch, so the downstream plugin route handler can see a truncated or hanging body depending on the runtime. Build fresh headers and dropcontent-length/content-encoding. -
The admin marketplace client treats an empty
mcpToolsarray as a consent-required error.getMcpConsentTools()returns[]when details are missing or tools are empty, and the callers checkif (mcpTools). An empty array is truthy, so a malformed/unrelated error response could surface as a consent dialog, and a genuinely empty consent response would loop because the follow-up request still doesn't confirm. Returnnullwhen there are no tools. -
The registry client helper has the same empty-array issue in
parseMcpConsent().
I also left two minor suggestions: a stale comment in the plugin catch-all route that still refers to plugins:manage, and the plugin-scoped token regex, which accepts characters that are not valid EmDash plugin identifiers.
What does this PR do?
Allows plugins to explicitly declare selected API routes as MCP tools, following the design approved in Discussion #1587.
Plugin authors can provide stable tool names, descriptions, input/output schemas, and MCP annotations. EmDash validates and bundles those declarations, exposes enabled tools through the existing MCP server with plugin-prefixed names, enforces each route's RBAC permission, and records invocation audit events.
Plugin MCP access is disabled by default and requires explicit administrator consent during install, update, or plugin management. Bearer tokens must also opt in with either
mcp:toolsor a plugin-scopedmcp:tools:<pluginId>scope; historicaladmintokens do not silently gain access to plugin-provided tools.This also adds authoring documentation, token-scope controls, migration coverage, and a small smoke-test plugin used by the site matrix.
Discussion: #1587
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
pnpm lintpnpm typecheckpnpm typecheck:demospnpm format:checkpnpm test: admin (1,100 tests), auth (57 tests), plugin CLI (394 tests), and core (4,715 tests) all pass.127.0.0.1:18788); the affected wall-time integration test passes when rerun in isolation.demos/simple: after enabling the smoke plugin, MCPtools/listadvertisedmcp-smoke__echowith its declared schema and annotations.