Skip to content

Add plugin-defined MCP tools#988

Open
masonjames wants to merge 6 commits into
emdash-cms:mainfrom
masonjames:codex/plugin-mcp-tools
Open

Add plugin-defined MCP tools#988
masonjames wants to merge 6 commits into
emdash-cms:mainfrom
masonjames:codex/plugin-mcp-tools

Conversation

@masonjames
Copy link
Copy Markdown
Contributor

What does this PR do?

Adds the approved plugin-defined MCP tool surface from Discussion #850:

  • adds mcp:tools as an opt-in plugin capability
  • adds mcpTools metadata to plugin manifests, descriptors, bundle extraction, and runtime plugin wiring
  • registers enabled plugin MCP tools on the existing EmDash MCP endpoint
  • delegates MCP tool execution to the plugin route dispatcher so route validation/context/sandbox paths are reused
  • requires admin token scope plus ADMIN role for plugin MCP tool calls

Related discussion: #850

Type of change

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

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5), RepoPrompt agent_run Deep Plan/review (GPT-5.5)

Screenshots / test output

  • pnpm typecheck
  • pnpm lint (exits 0; reports 39 existing warnings, 0 errors)
  • pnpm test
  • git diff --check
  • pnpm --filter emdash test -- tests/integration/mcp/plugin-tools.test.ts tests/unit/plugins/adapt-sandbox-entry.test.ts tests/unit/plugins/manifest-schema.test.ts tests/unit/plugins/define-plugin.test.ts tests/unit/cli/bundle-utils.test.ts
  • pnpm --filter @emdash-cms/registry-cli test -- bundle-utils.test.ts

Adversarial review was run twice with RepoPrompt agent_run. The first pass found two issues around unloaded sandboxed tools and plugin ID tool-name collisions; both were fixed and the second pass found no blocking issues.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: c66be2d

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

This PR includes changesets to release 15 packages
Name Type
emdash Patch
@emdash-cms/plugin-types Patch
@emdash-cms/registry-cli Patch
@emdash-cms/cloudflare Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@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 683 lines across 26 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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 11, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

emdash

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

create-emdash

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

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

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: c66be2d

@masonjames masonjames marked this pull request as ready for review May 11, 2026 13:57
@github-actions
Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic
Copy link
Copy Markdown
Collaborator

/review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Solid implementation overall — capability gating, hex-encoded tool names to avoid plugin-id collisions, admin scope + ADMIN role enforced before dispatch, and good test coverage (capability filter, sandbox-not-loaded guard, scope check, ID collision). The delegation to handlePluginApiRoute is a clean reuse strategy.

Findings are mostly DX/footgun concerns at the definePlugin boundary plus a couple of manifest cross-validation gaps. No correctness or auth bugs that block merge.

Notes:

  • Trusted (definePlugin) plugins skip several validations that sandboxed plugins get from pluginManifestSchema — tool-name shape, route existence, and capability declaration. Silent filtering vs. early errors.
  • getPluginRouteMeta falls back to { public: false } for any route on an existing sandboxed plugin, so MCP tools pointing at nonexistent sandboxed routes register fine and fail at call time. The catch-all route has the same behavior, so this matches existing semantics — flagging only as DX context.
  • One coverage gap: no test for requireRole(ADMIN) rejecting a non-admin user holding an admin-scoped token (the defense-in-depth branch).
  • Sandboxed plugin MCP tools have no input schema in ManifestMcpToolEntry, so they all fall back to z.record(z.string(), z.unknown()) — MCP clients lose introspection. Future work, but worth tracking.

Comment thread packages/core/src/plugins/define-plugin.ts
Comment thread packages/core/src/mcp/server.ts Outdated
Comment thread packages/core/src/plugins/manifest-schema.ts
Comment thread packages/core/tests/integration/mcp/plugin-tools.test.ts
Comment thread packages/core/src/mcp/server.ts
Comment thread packages/core/src/emdash-runtime.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 12, 2026

Review submitted successfully (review ID 4270626524) with 6 line-anchored comments and a summary body.

Key findings:

  • DX footguns at definePlugin: no validation of MCP tool name shape, route existence, or required mcp:tools capability — sandboxed plugins get all three from manifest schema, trusted plugins fail silently.
  • Manifest cross-validation gaps: mcpTools[i].route not cross-checked against routes; mcpTools non-empty not cross-checked against the capability declaration.
  • Test coverage gap: no test for the defense-in-depth requireRole(ADMIN) branch.
  • Schema introspection: sandboxed plugin tools fall back to z.record(string, unknown), losing client-side schema info.
  • Documentation suggestion: note that routeMeta.public is intentionally ignored in the MCP path.
  • Dedup gap: getPluginMcpTools doesn't dedup across the three sources; relies on registration-order semantics in createMcpServer.

No blocking issues — auth/correctness paths check out.

github run

Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

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

Excellent work. One thing that would be good would be if there was a way we could provide a schema for tool inputs so we can validate and describe them. While we can't serialise a Zod schema directly, maybe a JSON schema would work?

Can you include docs in the PR too, as it will also make it easier to review.

Comment thread packages/core/src/mcp/server.ts Outdated
@masonjames
Copy link
Copy Markdown
Contributor Author

Excellent work. One thing that would be good would be if there was a way we could provide a schema for tool inputs so we can validate and describe them. While we can't serialise a Zod schema directly, maybe a JSON schema would work?

Can you include docs in the PR too, as it will also make it easier to review.

Agreed on both.

I’ll add docs in this PR, and I’ll look at adding a constrained inputSchema field to ManifestMcpToolEntry for client-facing tool introspection. I’m inclined to keep route handlers as the runtime validation source of truth for now, so plugin tools still execute through the existing route validation/context path, while JSON Schema gives MCP clients useful parameter descriptions.

I’ll keep the JSON Schema subset conservative: object root, properties, required, additionalProperties, scalar/array types, enum, description, default, and basic string/number/array constraints.

No $ref/recursive schemas in this pass unless you think we should make that part of the initial contract.

@masonjames
Copy link
Copy Markdown
Contributor Author

Done. I added docs for plugin MCP tools and added manifest-side inputSchema support using a constrained JSON Schema subset. Trusted plugins can still pass Zod via input; manifest/sandboxed plugins can now provide serializable JSON Schema, which is converted to Zod at MCP registration for client metadata and call validation.

Comment thread packages/core/src/mcp/json-schema.ts
Comment thread packages/core/src/plugins/types.ts
Comment thread packages/core/src/emdash-runtime.ts
Comment thread packages/core/tests/integration/mcp/plugin-tools.test.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants