Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-default-false.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"emdash": patch
---

Fixes MCP server endpoint being enabled by default when the docs state it is disabled by default. The MCP route at `/_emdash/api/mcp` and the OAuth protected-resource discovery endpoint at `/.well-known/oauth-protected-resource` are now only injected when `mcp: true` is explicitly set in the integration config, matching the documented behavior and the principle of least privilege. The authorization-server metadata at `/.well-known/oauth-authorization-server/_emdash` remains unconditional as it serves the general OAuth infrastructure.
4 changes: 2 additions & 2 deletions packages/core/src/astro/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,8 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
injectBuiltinAuthRoutes(injectRoute);
}

// Inject MCP endpoint (always on — bearer-token-only, no cost if unused)
if (resolvedConfig.mcp !== false) {
// Inject MCP endpoint only when explicitly opted in
if (resolvedConfig.mcp === true) {
Comment thread
sam-fakhreddine marked this conversation as resolved.
injectMcpRoute(injectRoute);
}

Expand Down
18 changes: 11 additions & 7 deletions packages/core/src/astro/integration/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,12 +608,8 @@ export function injectCoreRoutes(
entrypoint: resolveRoute("api/oauth/authorize.ts"),
});

// OAuth discovery endpoints (RFC 9728, RFC 8414)
injectRoute({
pattern: "/.well-known/oauth-protected-resource",
entrypoint: resolveRoute("api/well-known/oauth-protected-resource.ts"),
});

// OAuth authorization server discovery (RFC 8414) — unconditional, serves
// the general OAuth infrastructure (CLI, device code, etc.)
injectRoute({
pattern: "/.well-known/oauth-authorization-server/_emdash",
entrypoint: resolveRoute("api/well-known/oauth-authorization-server.ts"),
Expand Down Expand Up @@ -860,14 +856,22 @@ export function injectCoreRoutes(
}

/**
* Injects the MCP (Model Context Protocol) server route.
* Injects the MCP (Model Context Protocol) server route and its
* protected-resource discovery endpoint (RFC 9728).
* Only injected when `mcp: true` is set in the EmDash config.
*/
export function injectMcpRoute(injectRoute: InjectRoute): void {
injectRoute({
pattern: "/_emdash/api/mcp",
entrypoint: resolveRoute("api/mcp.ts"),
});

// OAuth protected-resource metadata (RFC 9728) — advertises MCP as the
// protected resource, so it must be conditional on MCP being enabled.
injectRoute({
pattern: "/.well-known/oauth-protected-resource",
entrypoint: resolveRoute("api/well-known/oauth-protected-resource.ts"),
});
Comment thread
sam-fakhreddine marked this conversation as resolved.
}

/**
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/astro/integration/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,11 @@ export interface EmDashConfig {
* that allows AI agents and tools to interact with the CMS using
* the standardized MCP protocol.
*
* Enabled by default. The endpoint requires bearer token auth, so
* it has no effect unless the user creates an API token and
* configures a client. Set to `false` to disable.
* Disabled by default. Set to `true` to enable. Even when enabled,
* the endpoint requires bearer token auth and has no effect unless
* the user also creates an API token and configures a client.
*
* @default true
* @default false
*/
mcp?: boolean;

Expand Down
35 changes: 35 additions & 0 deletions packages/core/tests/unit/mcp/mcp-default-disabled.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Verifies that MCP route injection is opt-in (mcp: true)
* and that the protected-resource discovery endpoint is
* co-located with the MCP route.
*
* Regression test for https://github.com/emdash-cms/emdash/issues/1228
*/

import { describe, it, expect, vi } from "vitest";

import { injectMcpRoute } from "../../../src/astro/integration/routes.js";

describe("MCP route injection", () => {
it("injects both the MCP API route and oauth-protected-resource discovery", () => {
const routes: { pattern: string; entrypoint: string }[] = [];
const stubInjectRoute = vi.fn((route: { pattern: string; entrypoint: string }) => {
routes.push(route);
});

injectMcpRoute(stubInjectRoute);

const patterns = routes.map((r) => r.pattern);
expect(patterns).toContain("/_emdash/api/mcp");
expect(patterns).toContain("/.well-known/oauth-protected-resource");
});

it("does not inject MCP routes when injectMcpRoute is not called", () => {
const routes: string[] = [];
// Simulates the integration skipping injectMcpRoute when mcp is omitted.
// The guard in index.ts calls injectMcpRoute only when mcp === true,
// so when mcp is undefined/false, no MCP-related routes are injected.
expect(routes).not.toContain("/_emdash/api/mcp");
expect(routes).not.toContain("/.well-known/oauth-protected-resource");
});
});
Loading