Skip to content

Harden plugin MCP tool registration#988

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

Harden plugin MCP tool registration#988
masonjames wants to merge 14 commits into
emdash-cms:mainfrom
masonjames:codex/plugin-mcp-tools

Conversation

@masonjames

@masonjames masonjames commented May 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Resolves the branch against current main after #2002 landed the approved plugin MCP tool contract. The conflict resolution keeps that newer contract authoritative and retains two hardening improvements from this branch:

  • generates deterministic MCP-safe names for scoped plugin IDs while preserving existing names for already-safe unscoped IDs
  • isolates malformed serialized input/output schemas so one plugin cannot disable the entire MCP endpoint
  • documents both behaviors and adds focused regression coverage

This PR no longer introduces the earlier parallel mcpTools manifest surface.

Follow-up to #2002 and 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

  • 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 (not applicable; no admin UI strings changed). 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: not applicable; this is a bug-fix follow-up to feat(plugins): allow explicit MCP tool declarations #2002

AI-generated code disclosure

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

Screenshots / test output

  • pnpm typecheck
  • pnpm lint:json — 0 diagnostics
  • pnpm format
  • pnpm changeset status --since=origin/main
  • pnpm --filter emdash exec vitest run tests/unit/mcp/authorization.test.ts tests/unit/mcp/json-schema.test.ts tests/unit/mcp/plugin-tool-name.test.ts — 52 tests passed
  • GitHub CI passed against d6ff36871aeee3a12b2e36010e2b6db9d44a0d35, including unit, integration, smoke, browser, Node E2E, Cloudflare E2E, lint, typecheck, changeset, and security checks

Local full-core Vitest completed with 4,976 passing tests and one unchanged macOS temp-path normalization failure in virtual-modules.test.ts (/var/folders versus /private/var/folders). The focused tests and Linux CI do not reproduce it.

@changeset-bot

changeset-bot Bot commented May 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e7f2a51

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

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@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/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

pkg-pr-new Bot commented May 11, 2026

Copy link
Copy Markdown

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/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@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-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@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/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@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-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@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: e7f2a51

@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

@ask-bonk ask-bonk 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.

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 Outdated
Comment thread packages/core/src/mcp/server.ts Outdated
Comment thread packages/core/src/plugins/manifest-schema.ts Outdated
Comment thread packages/core/tests/integration/mcp/plugin-tools.test.ts Outdated
Comment thread packages/core/src/mcp/server.ts
Comment thread packages/core/src/emdash-runtime.ts
@ask-bonk

ask-bonk Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

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

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

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 Outdated
Comment thread packages/core/src/plugins/types.ts Outdated
Comment thread packages/core/src/emdash-runtime.ts Outdated
Comment thread packages/core/tests/integration/mcp/plugin-tools.test.ts Outdated
@ascorbic

Copy link
Copy Markdown
Collaborator

/review

@ask-bonk ask-bonk 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.

Reviewing on top of the existing rounds (ask-bonk + ascorbic). Most of the substantive concerns from the earlier passes — capability cross-check, route cross-check, tool-name shape validation, JSON Schema introspection, dual-check coverage, admin-only intent comment, runtime dedup — have been addressed. A few remaining things worth a look, all DX/robustness rather than auth/correctness:

  • One bad inputSchema from a trusted plugin can take down the whole MCP endpoint because jsonSchemaObjectToZod is called inline during createMcpServer and has no isolation. Trusted plugins skip the Zod manifest validation that sandboxed plugins get, so the JSON Schema is unvalidated until z.fromJSONSchema throws.
  • pluginToolName collision detection looks for __ within a single segment, but two distinct valid scoped IDs can still collapse to the same generated name when a dash sits on either side of the slash boundary (@a-/b vs @a/-b both → a___b__foo). Narrow but real.
  • No dedicated unit test file for pluginToolName — the integration tests cover the happy paths but the ambiguous-segment / hash-fallback branches aren't directly exercised.

No blocking issues from me.

Comment thread packages/core/src/mcp/server.ts Outdated
Comment thread packages/core/src/mcp/plugin-tool-name.ts Outdated
Comment thread packages/core/src/mcp/plugin-tool-name.ts
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity.

If you're still working on this, please push an update or leave a comment.

@github-actions

Copy link
Copy Markdown
Contributor

This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity.

If you're still working on this, please push an update or leave a comment.

# Conflicts:
#	packages/core/src/astro/integration/runtime.ts
#	packages/core/src/astro/integration/virtual-modules.ts
#	packages/core/src/astro/middleware.ts
#	packages/core/src/astro/routes/api/mcp.ts
#	packages/core/src/cli/commands/bundle-utils.ts
#	packages/core/src/emdash-runtime.ts
#	packages/core/src/mcp/server.ts
#	packages/core/src/plugin-types.ts
#	packages/core/src/plugins/adapt-sandbox-entry.ts
#	packages/core/src/plugins/define-plugin.ts
#	packages/core/src/plugins/manifest-schema.ts
#	packages/core/src/plugins/types.ts
#	packages/core/tests/unit/astro/middleware-prerender.test.ts
#	packages/core/tests/unit/cli/bundle-utils.test.ts
#	packages/core/tests/unit/plugins/manifest-schema.test.ts
#	packages/plugin-cli/src/bundle/types.ts
#	packages/plugin-cli/src/bundle/utils.ts
#	packages/plugin-cli/tests/bundle-utils.test.ts
#	packages/plugin-types/src/index.ts
#	pnpm-lock.yaml
@masonjames masonjames changed the title Add plugin-defined MCP tools Harden plugin MCP tool registration Jul 21, 2026
@masonjames masonjames added review/ready-for-maintainer and removed review/needs-rereview Author pushed changes since the last review labels Jul 21, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 21, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 21, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review needs-rebase labels Jul 21, 2026
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