diff --git a/mcp/README.md b/mcp/README.md index ba90710b..7ec5963d 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -16,13 +16,10 @@ A Cloudflare Worker that implements the [Model Context Protocol (MCP)](https://m "mcp-remote", "https://mcp.honcho.dev", "--header", - "Authorization:${AUTH_HEADER}", - "--header", - "X-Honcho-User-Name:${USER_NAME}" + "Authorization:${AUTH_HEADER}" ], "env": { - "AUTH_HEADER": "Bearer ", - "USER_NAME": "" + "AUTH_HEADER": "Bearer " } } } @@ -115,8 +112,7 @@ bun run tsc --noEmit ```bash bunx mcp-remote http://localhost:8787 \ - --header "Authorization:Bearer " \ - --header "X-Honcho-User-Name:test" + --header "Authorization:Bearer " ``` ### Deploy diff --git a/mcp/src/config.ts b/mcp/src/config.ts index 58e4307e..98cf8760 100644 --- a/mcp/src/config.ts +++ b/mcp/src/config.ts @@ -2,8 +2,6 @@ import { Honcho } from "@honcho-ai/sdk"; export interface HonchoConfig { apiKey: string; - userName: string; - assistantName: string; baseUrl: string; workspaceId: string; } @@ -14,7 +12,7 @@ export interface Env { /** * Parse configuration from request headers and Worker env bindings. - * Throws on missing required fields so callers get clear errors. + * Throws only when the Authorization bearer token is missing/empty. * * The Honcho API URL is read from the `HONCHO_API_URL` env var when set, * allowing operators to run this Worker alongside a self-hosted Honcho @@ -24,29 +22,19 @@ export interface Env { */ export function parseConfig(request: Request, env: Env = {}): HonchoConfig { const authHeader = request.headers.get("Authorization"); - const trimmedAuthHeader = authHeader?.trim(); - if (!trimmedAuthHeader?.startsWith("Bearer ")) { + const bearerMatch = authHeader?.trim().match(/^Bearer\s+(.*)$/i); + if (!bearerMatch) { throw new Error( "Missing Authorization header. Provide 'Authorization: Bearer '.", ); } - const apiKey = trimmedAuthHeader.substring(7).trim(); + const apiKey = bearerMatch[1].trim(); if (!apiKey) { throw new Error("Authorization header is empty after 'Bearer '."); } - const rawUserName = request.headers.get("X-Honcho-User-Name"); - const userName = rawUserName?.trim(); - if (!userName) { - throw new Error( - "Missing X-Honcho-User-Name header. Provide 'X-Honcho-User-Name: '.", - ); - } - return { apiKey, - userName, - assistantName: request.headers.get("X-Honcho-Assistant-Name")?.trim() || "Assistant", baseUrl: env.HONCHO_API_URL?.trim() || "https://api.honcho.dev", workspaceId: request.headers.get("X-Honcho-Workspace-ID")?.trim() || "default", }; diff --git a/mcp/src/index.ts b/mcp/src/index.ts index 4e895198..a7e0f107 100644 --- a/mcp/src/index.ts +++ b/mcp/src/index.ts @@ -5,14 +5,25 @@ import { createServer } from "./server.js"; const CORS_ORIGIN = "*"; const CORS_METHODS = "GET, POST, DELETE, OPTIONS"; const CORS_ALLOWED_HEADERS = - "Content-Type, Authorization, X-Honcho-User-Name, X-Honcho-Workspace-ID, X-Honcho-Assistant-Name"; + "Content-Type, Authorization, X-Honcho-Workspace-ID"; const CORS_HEADERS = { "Access-Control-Allow-Origin": CORS_ORIGIN, "Access-Control-Allow-Methods": CORS_METHODS, "Access-Control-Allow-Headers": CORS_ALLOWED_HEADERS, + "Access-Control-Expose-Headers": "WWW-Authenticate", }; +const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource"; + +function resourceUrl(request: Request): string { + return new URL(request.url).origin; +} + +function authorizationServer(env: Env): string { + return env.HONCHO_API_URL?.trim() || "https://api.honcho.dev"; +} + export default { async fetch( request: Request, @@ -23,15 +34,34 @@ export default { return new Response(null, { status: 204, headers: CORS_HEADERS }); } + // Protected Resource Metadata (RFC 9728) — served without auth so clients + // can discover the authorization server. + if (new URL(request.url).pathname === PROTECTED_RESOURCE_PATH) { + return Response.json( + { + resource: resourceUrl(request), + authorization_servers: [authorizationServer(env)], + bearer_methods_supported: ["header"], + }, + { headers: CORS_HEADERS }, + ); + } + let config; try { config = parseConfig(request, env); } catch (e) { const message = e instanceof Error ? e.message : "Invalid request"; + // WWW-Authenticate points clients at the metadata so they start the OAuth flow. + const resourceMetadata = `${resourceUrl(request)}${PROTECTED_RESOURCE_PATH}`; return new Response(JSON.stringify({ error: message }), { status: 401, - headers: { "Content-Type": "application/json", ...CORS_HEADERS }, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadata}"`, + ...CORS_HEADERS, + }, }); }