diff --git a/test-board.yaml b/test-board.yaml new file mode 100644 index 0000000..a156ca0 --- /dev/null +++ b/test-board.yaml @@ -0,0 +1,22 @@ +version: 1 +project: + name: "substack-mcp" + test_command: "npm run type-check" + repo: "nanameru/substack-mcp" + +source_roots: + - worker + - src + +cases: + - id: TC-001 + title: "Cloudflare Remote MCP本番デプロイ: OAuth保護されたMCPへ接続し読み取り専用で下書きを1件取得する" + feature: "Cloudflare Remote MCP本番デプロイ" + scenario: "OAuth保護されたMCPへ接続し読み取り専用で下書きを1件取得する" + status: passed + priority: high + type: e2e + source: [worker/index.ts, worker/oauth.ts, src/substack_mcp/cloud_api.py, wrangler.jsonc] + test_file: "" + issues: [6] + notes: "2026-09-12にChatGPTからOAuth接続し、11ツールの認識とlist_drafts(limit=1)の成功を確認。記事本文は表示せず、書き込み系ツールは未実行。" diff --git a/worker/index.ts b/worker/index.ts index 6471de1..c5d346c 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -211,13 +211,18 @@ function createServer() { return server; } -const apiHandler = createMcpHandler(createServer); +const handleMcpRequest = createMcpHandler(createServer); +const apiHandler = { + fetch(request: Request, workerEnv: Env, ctx: ExecutionContext) { + return handleMcpRequest(request, workerEnv, ctx); + }, +} satisfies ExportedHandler; export default new OAuthProvider({ authorizeEndpoint: "/authorize", tokenEndpoint: "/oauth/token", clientRegistrationEndpoint: "/oauth/register", apiRoute: "/mcp", - apiHandler: apiHandler as any, + apiHandler, defaultHandler: authHandler, }); diff --git a/worker/oauth.ts b/worker/oauth.ts index ed3de0d..b136fc9 100644 --- a/worker/oauth.ts +++ b/worker/oauth.ts @@ -92,7 +92,7 @@ async function authorizeGet(request: Request, env: OAuthEnv): Promise return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8", - "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'", + "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self' https://github.com/login/oauth/authorize; base-uri 'none'; frame-ancestors 'none'", "Referrer-Policy": "no-referrer", "X-Frame-Options": "DENY", "Set-Cookie": secureCookie(CSRF_COOKIE, csrf, 600), @@ -161,8 +161,22 @@ async function callback(request: Request, env: OAuthEnv): Promise { }), }); if (!tokenResponse.ok) return jsonError("GitHub token exchange failed", 502); - const tokenData = (await tokenResponse.json()) as { access_token?: string }; - if (!tokenData.access_token) return jsonError("GitHub did not return an access token", 502); + const tokenData = (await tokenResponse.json()) as { + access_token?: string; + error?: string; + }; + if (!tokenData.access_token) { + // GitHub commonly returns HTTP 200 with a machine-readable OAuth error. + // Surface only that non-secret error code so credential/configuration issues + // can be distinguished without logging the authorization code or secrets. + const oauthError = tokenData.error?.replace(/[^a-z0-9_]/gi, "").slice(0, 80); + return jsonError( + oauthError + ? `GitHub token exchange failed: ${oauthError}` + : "GitHub did not return an access token", + 502, + ); + } const userResponse = await fetch("https://api.github.com/user", { headers: { diff --git a/wrangler.jsonc b/wrangler.jsonc index a0ce727..79537c1 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -32,7 +32,7 @@ "kv_namespaces": [ { "binding": "OAUTH_KV", - "id": "REPLACE_WITH_OAUTH_KV_ID" + "id": "2804e83ca60f4b6487b038442fed4779" } ] }