feat: support OAuth for MCP clients.

This commit is contained in:
ajspig 2026-07-21 10:32:46 -04:00
parent 063aaa97a6
commit f3fb03caf8
3 changed files with 35 additions and 22 deletions

View File

@ -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 <your-honcho-key>",
"USER_NAME": "<your-name>"
"AUTH_HEADER": "Bearer <your-honcho-key>"
}
}
}
@ -115,8 +112,7 @@ bun run tsc --noEmit
```bash
bunx mcp-remote http://localhost:8787 \
--header "Authorization:Bearer <key>" \
--header "X-Honcho-User-Name:test"
--header "Authorization:Bearer <key>"
```
### Deploy

View File

@ -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
@ -35,18 +33,8 @@ export function parseConfig(request: Request, env: Env = {}): HonchoConfig {
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: <your-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",
};

View File

@ -5,7 +5,7 @@ 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,
@ -13,6 +13,16 @@ const CORS_HEADERS = {
"Access-Control-Allow-Headers": CORS_ALLOWED_HEADERS,
};
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 +33,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,
},
});
}