Skip to content

Commit 4f9a413

Browse files
authored
feat: support OAuth for MCP clients. (#923)
* feat: support OAuth for MCP clients. * fix: scheme is case-insensitive. * fix: expose WWW-Authenticate header for cross-origin clients.
1 parent 063aaa9 commit 4f9a413

3 files changed

Lines changed: 39 additions & 25 deletions

File tree

mcp/README.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@ A Cloudflare Worker that implements the [Model Context Protocol (MCP)](https://m
1616
"mcp-remote",
1717
"https://mcp.honcho.dev",
1818
"--header",
19-
"Authorization:${AUTH_HEADER}",
20-
"--header",
21-
"X-Honcho-User-Name:${USER_NAME}"
19+
"Authorization:${AUTH_HEADER}"
2220
],
2321
"env": {
24-
"AUTH_HEADER": "Bearer <your-honcho-key>",
25-
"USER_NAME": "<your-name>"
22+
"AUTH_HEADER": "Bearer <your-honcho-key>"
2623
}
2724
}
2825
}
@@ -115,8 +112,7 @@ bun run tsc --noEmit
115112

116113
```bash
117114
bunx mcp-remote http://localhost:8787 \
118-
--header "Authorization:Bearer <key>" \
119-
--header "X-Honcho-User-Name:test"
115+
--header "Authorization:Bearer <key>"
120116
```
121117

122118
### Deploy

mcp/src/config.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { Honcho } from "@honcho-ai/sdk";
22

33
export interface HonchoConfig {
44
apiKey: string;
5-
userName: string;
6-
assistantName: string;
75
baseUrl: string;
86
workspaceId: string;
97
}
@@ -14,7 +12,7 @@ export interface Env {
1412

1513
/**
1614
* Parse configuration from request headers and Worker env bindings.
17-
* Throws on missing required fields so callers get clear errors.
15+
* Throws only when the Authorization bearer token is missing/empty.
1816
*
1917
* The Honcho API URL is read from the `HONCHO_API_URL` env var when set,
2018
* allowing operators to run this Worker alongside a self-hosted Honcho
@@ -24,29 +22,19 @@ export interface Env {
2422
*/
2523
export function parseConfig(request: Request, env: Env = {}): HonchoConfig {
2624
const authHeader = request.headers.get("Authorization");
27-
const trimmedAuthHeader = authHeader?.trim();
28-
if (!trimmedAuthHeader?.startsWith("Bearer ")) {
25+
const bearerMatch = authHeader?.trim().match(/^Bearer\s+(.*)$/i);
26+
if (!bearerMatch) {
2927
throw new Error(
3028
"Missing Authorization header. Provide 'Authorization: Bearer <your-honcho-key>'.",
3129
);
3230
}
33-
const apiKey = trimmedAuthHeader.substring(7).trim();
31+
const apiKey = bearerMatch[1].trim();
3432
if (!apiKey) {
3533
throw new Error("Authorization header is empty after 'Bearer '.");
3634
}
3735

38-
const rawUserName = request.headers.get("X-Honcho-User-Name");
39-
const userName = rawUserName?.trim();
40-
if (!userName) {
41-
throw new Error(
42-
"Missing X-Honcho-User-Name header. Provide 'X-Honcho-User-Name: <your-name>'.",
43-
);
44-
}
45-
4636
return {
4737
apiKey,
48-
userName,
49-
assistantName: request.headers.get("X-Honcho-Assistant-Name")?.trim() || "Assistant",
5038
baseUrl: env.HONCHO_API_URL?.trim() || "https://api.honcho.dev",
5139
workspaceId: request.headers.get("X-Honcho-Workspace-ID")?.trim() || "default",
5240
};

mcp/src/index.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,25 @@ import { createServer } from "./server.js";
55
const CORS_ORIGIN = "*";
66
const CORS_METHODS = "GET, POST, DELETE, OPTIONS";
77
const CORS_ALLOWED_HEADERS =
8-
"Content-Type, Authorization, X-Honcho-User-Name, X-Honcho-Workspace-ID, X-Honcho-Assistant-Name";
8+
"Content-Type, Authorization, X-Honcho-Workspace-ID";
99

1010
const CORS_HEADERS = {
1111
"Access-Control-Allow-Origin": CORS_ORIGIN,
1212
"Access-Control-Allow-Methods": CORS_METHODS,
1313
"Access-Control-Allow-Headers": CORS_ALLOWED_HEADERS,
14+
"Access-Control-Expose-Headers": "WWW-Authenticate",
1415
};
1516

17+
const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
18+
19+
function resourceUrl(request: Request): string {
20+
return new URL(request.url).origin;
21+
}
22+
23+
function authorizationServer(env: Env): string {
24+
return env.HONCHO_API_URL?.trim() || "https://api.honcho.dev";
25+
}
26+
1627
export default {
1728
async fetch(
1829
request: Request,
@@ -23,15 +34,34 @@ export default {
2334
return new Response(null, { status: 204, headers: CORS_HEADERS });
2435
}
2536

37+
// Protected Resource Metadata (RFC 9728) — served without auth so clients
38+
// can discover the authorization server.
39+
if (new URL(request.url).pathname === PROTECTED_RESOURCE_PATH) {
40+
return Response.json(
41+
{
42+
resource: resourceUrl(request),
43+
authorization_servers: [authorizationServer(env)],
44+
bearer_methods_supported: ["header"],
45+
},
46+
{ headers: CORS_HEADERS },
47+
);
48+
}
49+
2650
let config;
2751
try {
2852
config = parseConfig(request, env);
2953
} catch (e) {
3054
const message =
3155
e instanceof Error ? e.message : "Invalid request";
56+
// WWW-Authenticate points clients at the metadata so they start the OAuth flow.
57+
const resourceMetadata = `${resourceUrl(request)}${PROTECTED_RESOURCE_PATH}`;
3258
return new Response(JSON.stringify({ error: message }), {
3359
status: 401,
34-
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
60+
headers: {
61+
"Content-Type": "application/json",
62+
"WWW-Authenticate": `Bearer resource_metadata="${resourceMetadata}"`,
63+
...CORS_HEADERS,
64+
},
3565
});
3666
}
3767

0 commit comments

Comments
 (0)