Skip to content

Commit 9b43bfc

Browse files
authored
fix(worker): support Claude CIMD metadata (#961)
* fix(worker): support Claude CIMD metadata * test(worker): tighten Claude OAuth regression test * test(worker): type Claude fetch mock
1 parent 66aca90 commit 9b43bfc

5 files changed

Lines changed: 72 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hevy-mcp/worker": patch
3+
---
4+
5+
Fix Claude OAuth compatibility by accepting CIMD documents that advertise optional unsupported grant types.

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"@changesets/changelog-github": "^0.7.0",
8181
"@changesets/cli": "3.0.0-next.11",
8282
"@cloudflare/vitest-pool-workers": "^0.20.2",
83-
"@cloudflare/workers-oauth-provider": "^0.10.0",
83+
"@cloudflare/workers-oauth-provider": "^0.10.2",
8484
"@codecov/rollup-plugin": "^2.0.1",
8585
"@commitlint/cli": "^21.2.1",
8686
"@commitlint/config-conventional": "^21.2.0",

packages/worker/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"check:types": "tsc --noEmit -p tsconfig.json"
1515
},
1616
"dependencies": {
17-
"@cloudflare/workers-oauth-provider": "^0.10.0",
17+
"@cloudflare/workers-oauth-provider": "^0.10.2",
1818
"@modelcontextprotocol/server": "^2.0.0",
1919
"zod": "^4.4.3"
2020
},

packages/worker/src/worker-oauth.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,66 @@ describe("OAuth-enabled Worker fetch handler", () => {
917917
expect(rejected.status).toBe(401);
918918
});
919919

920+
it("accepts Claude's published CIMD metadata with an optional JWT grant", async () => {
921+
const clientId = "https://claude.ai/oauth/mcp-oauth-client-metadata";
922+
const metadata = {
923+
client_id: clientId,
924+
client_name: "Claude",
925+
client_uri: "https://claude.ai",
926+
redirect_uris: [redirectUri],
927+
grant_types: [
928+
"authorization_code",
929+
"refresh_token",
930+
"urn:ietf:params:oauth:grant-type:jwt-bearer",
931+
],
932+
response_types: ["code"],
933+
token_endpoint_auth_method: "none",
934+
};
935+
const fetchMock = vi.fn((_input: RequestInfo | URL) =>
936+
Promise.resolve(Response.json(metadata)),
937+
);
938+
vi.stubGlobal("fetch", fetchMock);
939+
const { handler, env } = createHandlerWithEnv();
940+
const verifier = base64UrlEncode(
941+
crypto.getRandomValues(new Uint8Array(32)),
942+
);
943+
const challenge = base64UrlEncode(
944+
new Uint8Array(
945+
await crypto.subtle.digest(
946+
"SHA-256",
947+
new TextEncoder().encode(verifier),
948+
),
949+
),
950+
);
951+
const authorizeUrl = new URL("https://worker.example/authorize");
952+
authorizeUrl.searchParams.set("response_type", "code");
953+
authorizeUrl.searchParams.set("client_id", clientId);
954+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
955+
authorizeUrl.searchParams.set("code_challenge", challenge);
956+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
957+
authorizeUrl.searchParams.set("state", "claude-state");
958+
authorizeUrl.searchParams.set("scope", "mcp");
959+
authorizeUrl.searchParams.set("resource", "https://worker.example/mcp");
960+
961+
const result = await handler(new Request(authorizeUrl), env, {});
962+
963+
// Regression coverage for issue #942: provider 0.10.0 rejected
964+
// Claude's optional JWT grant; 0.10.2 negotiates it away and renders
965+
// the consent page.
966+
expect(result.status).toBe(200);
967+
expect(await result.text()).toContain("Claude");
968+
expect(fetchMock).toHaveBeenCalled();
969+
for (const [input] of fetchMock.mock.calls) {
970+
const requestedUrl =
971+
input instanceof Request
972+
? input.url
973+
: input instanceof URL
974+
? input.href
975+
: input;
976+
expect(requestedUrl).toBe(clientId);
977+
}
978+
});
979+
920980
it("completes the CIMD OAuth flow and serves MCP requests", async () => {
921981
const clientId = "https://chatgpt.com/oauth/hevy-mcp/client.json";
922982
const cimdRedirectUri = "https://chatgpt.com/connector/oauth/test-callback";

0 commit comments

Comments
 (0)