Closes the two code-side gaps found while auditing the Claude Connectors Directory submission checklist after #682/#683: 1. Origin-header validation on the /mcp endpoint (POST/GET/DELETE) — an explicit directory submission requirement and an MCP spec MUST for the Streamable HTTP transport (DNS-rebinding defense). Requests without an Origin header (claude.ai backend, Claude Desktop, npx gnubok-mcp, Claude Code, MCP Inspector's proxy — every known client) pass through unchanged. A present Origin is allowed only when its host matches the request Host (covers Vercel previews + self-hosted without hardcoding) or NEXT_PUBLIC_APP_URL (proxy-rewritten Host); anything else is 403 with a JSON-RPC error envelope. The endpoint sets no CORS headers, so no currently-working browser flow is affected. 2. serverInfo.title: 'Accounted' (MCP 2025-06-18 display name). name stays 'gnubok' — stable identifier clients may key state on. 3. export-docs-to-website.mts now also exports CONNECT_CLAUDE_MD to the gnubok-website repo, so docs.gnubok.se/connect-claude (the target of the canonical /docs/api redirect) stays in sync. Companion website PR: jakobwennberg/gnubok-website#1. Tests: new origin-guard.test.ts (10 tests — no-Origin pass-through, same-origin, preview host, proxy host via env, foreign/port-mismatch/ null/malformed rejection, 403 envelope, and per-method enforcement on the registered apiRoutes). Full MCP suite 295/295 green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
import type { Extension } from '@/lib/extensions/types'
|
|
import { handleMcpRequest, tools as mcpTools } from './server'
|
|
import { isForbiddenOrigin, forbiddenOriginResponse } from './origin-guard'
|
|
import { registerAgentTools } from '@/lib/agent/tools/registry'
|
|
import type { AgentTool } from '@/lib/agent/tools/types'
|
|
|
|
// Make the same tool set available to the in-app chat agent. The chat loop
|
|
// (lib/agent/chat/*) dispatches against the core agentToolRegistry so it can
|
|
// stay decoupled from this extension's module path. Tools satisfy the
|
|
// AgentTool contract structurally — see lib/agent/tools/types.ts.
|
|
registerAgentTools(mcpTools as unknown as AgentTool[])
|
|
|
|
export const mcpServerExtension: Extension = {
|
|
id: 'mcp-server',
|
|
name: 'MCP Server',
|
|
version: '1.0.0',
|
|
|
|
settingsPanel: {
|
|
label: 'MCP-server (API)',
|
|
path: '/settings/api',
|
|
},
|
|
|
|
apiRoutes: [
|
|
{
|
|
method: 'POST',
|
|
path: '/mcp',
|
|
skipAuth: true, // Auth handled via API key in the handler
|
|
handler: async (request: Request) => {
|
|
// MCP spec MUST: validate Origin (DNS-rebinding defense). See origin-guard.ts.
|
|
if (isForbiddenOrigin(request)) return forbiddenOriginResponse()
|
|
return handleMcpRequest(request)
|
|
},
|
|
},
|
|
// MCP Streamable HTTP also needs GET for SSE and DELETE for session termination
|
|
{
|
|
method: 'GET',
|
|
path: '/mcp',
|
|
skipAuth: true,
|
|
handler: async (request: Request) => {
|
|
if (isForbiddenOrigin(request)) return forbiddenOriginResponse()
|
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
return new Response('Authorization required', {
|
|
status: 401,
|
|
headers: {
|
|
'WWW-Authenticate': `Bearer resource_metadata="${appUrl}/.well-known/oauth-protected-resource"`,
|
|
},
|
|
})
|
|
},
|
|
},
|
|
{
|
|
method: 'DELETE',
|
|
path: '/mcp',
|
|
skipAuth: true,
|
|
// Stateless — no sessions to terminate
|
|
handler: async (request: Request) => {
|
|
if (isForbiddenOrigin(request)) return forbiddenOriginResponse()
|
|
return new Response(null, { status: 204 })
|
|
},
|
|
},
|
|
],
|
|
|
|
eventHandlers: [],
|
|
}
|