Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions test-board.yaml
Original file line number Diff line number Diff line change
@@ -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)の成功を確認。記事本文は表示せず、書き込み系ツールは未実行。"
9 changes: 7 additions & 2 deletions worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Env>;

export default new OAuthProvider({
authorizeEndpoint: "/authorize",
tokenEndpoint: "/oauth/token",
clientRegistrationEndpoint: "/oauth/register",
apiRoute: "/mcp",
apiHandler: apiHandler as any,
apiHandler,
defaultHandler: authHandler,
});
20 changes: 17 additions & 3 deletions worker/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async function authorizeGet(request: Request, env: OAuthEnv): Promise<Response>
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),
Expand Down Expand Up @@ -161,8 +161,22 @@ async function callback(request: Request, env: OAuthEnv): Promise<Response> {
}),
});
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: {
Expand Down
2 changes: 1 addition & 1 deletion wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"kv_namespaces": [
{
"binding": "OAUTH_KV",
"id": "REPLACE_WITH_OAUTH_KV_ID"
"id": "2804e83ca60f4b6487b038442fed4779"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Security Risk: Production KV namespace ID is hardcoded in version control. If this repository is or becomes public, this exposes your production OAuth storage infrastructure. Consider using Wrangler secrets or environment-specific configuration files that are gitignored.

}
]
}