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
31 changes: 29 additions & 2 deletions src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export class MotionMCPAgent extends McpAgent<Env> {
registry
);
const enabledTools = configurator.getEnabledTools();
validator.initializeValidators(enabledTools);
// No AJV validator init here: ajv.compile() uses runtime code generation,
// which Cloudflare Workers disallows (EvalError). Input validation in the
// Worker is handled by the Zod schemas passed to server.tool() below;
// validateInput() is only called from the stdio entry point.

for (const tool of enabledTools) {
const zodShape = jsonSchemaToZodShape(tool.inputSchema as Parameters<typeof jsonSchemaToZodShape>[0]);
Expand Down Expand Up @@ -67,6 +70,22 @@ export default {
// Validate secret path: /mcp/{secret}/...
// Clients configure URL as: https://your-worker.workers.dev/mcp/YOUR_SECRET
const pathParts = url.pathname.split("/").filter(Boolean);

// SSE message endpoint: the SSE stream's `endpoint` event advertises the
// rewritten path (/mcp/message?sessionId=...), which has no secret segment.
// The sessionId is unguessable and only issued on a stream opened with the
// secret, so it authenticates these POSTs.
if (
pathParts[0] === "mcp" &&
pathParts[1] === "message" &&
request.method === "POST" &&
url.searchParams.has("sessionId")
) {
return (
MotionMCPAgent.mount("/mcp") as { fetch: (req: Request, env: Env, ctx: ExecutionContext) => Promise<Response> }
).fetch(request, env, ctx);
}

if (pathParts[0] !== "mcp" || pathParts[1] !== env.MOTION_MCP_SECRET) {
return new Response("Not found", { status: 404 });
}
Expand All @@ -77,8 +96,16 @@ export default {
const cleanUrl = new URL(cleanPath, url.origin);
const cleanRequest = new Request(cleanUrl, request);

// Streamable HTTP (POST/DELETE /mcp, or GET with an mcp-session-id header)
// is served by serve(); a bare GET on /mcp is a legacy SSE stream via mount().
const isStreamableHttp =
cleanPath === "/mcp" &&
(request.method !== "GET" || request.headers.has("mcp-session-id"));

return (
MotionMCPAgent.mount("/mcp") as { fetch: (req: Request, env: Env, ctx: ExecutionContext) => Promise<Response> }
(isStreamableHttp
? MotionMCPAgent.serve("/mcp")
: MotionMCPAgent.mount("/mcp")) as { fetch: (req: Request, env: Env, ctx: ExecutionContext) => Promise<Response> }
).fetch(cleanRequest, env, ctx);
},
};
2 changes: 1 addition & 1 deletion wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ bindings = [

[[migrations]]
tag = "v1"
new_classes = ["MotionMCPAgent"]
new_sqlite_classes = ["MotionMCPAgent"]

[vars]
MOTION_MCP_TOOLS = "essential"
Expand Down