Skip to content

feat(plugins): allow explicit MCP tool declarations#2002

Open
jcheese1 wants to merge 2 commits into
emdash-cms:mainfrom
jcheese1:plugin-mcp-tools
Open

feat(plugins): allow explicit MCP tool declarations#2002
jcheese1 wants to merge 2 commits into
emdash-cms:mainfrom
jcheese1:plugin-mcp-tools

Conversation

@jcheese1

Copy link
Copy Markdown
Contributor

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:tools or a plugin-scoped mcp:tools:<pluginId> scope; historical admin tokens 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

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: RFC: Let plugins explicitly declare MCP tools #1587

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5)

Screenshots / test output

  • pnpm lint
  • pnpm typecheck
  • pnpm typecheck:demos
  • pnpm format:check
  • Feature-relevant suites from pnpm test: admin (1,100 tests), auth (57 tests), plugin CLI (394 tests), and core (4,715 tests) all pass.
  • The aggregate test run encountered an unrelated parallel port collision in the untouched workerd package (127.0.0.1:18788); the affected wall-time integration test passes when rerun in isolation.
  • Live smoke check in demos/simple: after enabling the smoke plugin, MCP tools/list advertised mcp-smoke__echo with its declared schema and annotations.

@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 11fcd44

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/plugin-cli Minor
@emdash-cms/plugin-types Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Major
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/plugin-embeds Patch

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

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,733 lines across 76 files. Large PRs are harder to review and more likely to be closed without review.
This PR spans 5 different areas (area/core, area/admin, area/plugins, area/docs, area/auth). Consider breaking it into smaller, focused PRs.

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.

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2002

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2002

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2002

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2002

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2002

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2002

emdash

npm i https://pkg.pr.new/emdash@2002

create-emdash

npm i https://pkg.pr.new/create-emdash@2002

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2002

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2002

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2002

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2002

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2002

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2002

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2002

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2002

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2002

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2002

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2002

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2002

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2002

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2002

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2002

commit: 11fcd44

@github-actions

Copy link
Copy Markdown
Contributor

@swissky swissky added area/templates bot:review Trigger an emdashbot code review on this PR and removed area/templates labels Jul 15, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. handlePluginMcpTool reuses the incoming Content-Length header for a new, shorter request body. The internal Request copies request.headers verbatim while replacing the body with JSON.stringify(input). The original Content-Length corresponds 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 drop content-length/content-encoding.

  2. The admin marketplace client treats an empty mcpTools array as a consent-required error. getMcpConsentTools() returns [] when details are missing or tools are empty, and the callers check if (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. Return null when there are no tools.

  3. 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.

Comment thread packages/core/src/emdash-runtime.ts
Comment thread packages/admin/src/lib/api/marketplace.ts Outdated
Comment thread packages/admin/src/lib/api/registry.ts Outdated
Comment thread packages/core/src/astro/routes/api/plugins/[pluginId]/[...path].ts Outdated
Comment thread packages/auth/src/tokens.ts Outdated
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 15, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin area/auth area/core area/docs area/plugins bot:review Trigger an emdashbot code review on this PR overlap review/needs-rereview Author pushed changes since the last review size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants