Skip to content
Merged
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
10 changes: 10 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ Cloudflare Workers 上で動く、GitHub の issue / pull request / release / do
- いま何が起きたかを push で受け取ること
- 次に必要な状態を hybrid retrieval (dense + sparse) で引き戻すこと

## 破壊的変更: MCP プロトコル版 2026-07-28

このリリースから、Worker は **MCP プロトコル版 2026-07-28 のみ**を提供します。旧版向けの互換レーンは持ちません。

- **このリリースより古いブリッジは動作しなくなります。** 旧ブリッジは `initialize` でセッションを開こうとしますが、Worker はもう応答しません。落ち方は静かです。ブリッジはクラッシュせず、プロトコルエラーをツール出力のテキストとして返します。
- **新しいブリッジを取り込むには Claude Desktop を再起動してください。** ブリッジは `npx` で起動され、`@latest` はプロセス開始時に解決されます。すでに起動している Claude Desktop は、どれだけ新しい版が公開されていても起動時のコピーを保持し続けます。完全に終了してから開き直してください。
- **ブリッジの版を固定していると復旧しません。** MCP クライアント設定でこのリリースより古い版を pin している場合、再起動しても解決しません。まず pin を外す(または進める)必要があります。

Worker とブリッジは同一リリースで出るため、このリリース以降のブリッジであれば設定変更は不要です。

## Memory Model

このプロジェクトは GitHub を AI 作業の visible state store として扱います。
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ It is the search-oriented counterpart to [github-webhook-mcp](https://github.com
- push-based awareness of what just happened
- hybrid retrieval (dense + sparse) of the state that matters for the next step

## Breaking change: MCP protocol revision 2026-07-28

From this release the Worker serves **MCP protocol revision 2026-07-28 only**. It keeps no compatibility lane for the previous revision.

- **Bridge versions older than this release stop working.** They open a session with `initialize`, which the Worker no longer answers. The failure is quiet: the bridge does not crash, it returns the protocol error as tool output text.
- **Restart Claude Desktop to pick up the new bridge.** The bridge is launched with `npx`, and `@latest` is resolved at process start — an already-running Claude Desktop keeps the copy it started with, however new the published version is. Quit it fully and reopen.
- **Pinning the bridge version leaves you stuck.** If your MCP client config pins a version older than this release, restarting does not help; remove the pin (or move it forward) first.

The Worker and the bridge ship together, so a bridge from this release or later needs no configuration change.

## Memory Model

The project treats GitHub as a visible state store for AI work.
Expand Down
35 changes: 35 additions & 0 deletions docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,41 @@ params の静的ミラーから応答する(起動時を auth/network なし
enum から `wiki_doc` が抜けたまま気付けず、Worker は受け付けるのにクライアント側で
値が弾かれていた(gh#181)。

#### プロトコル版 2026-07-28、単レーン

Worker が提供する MCP プロトコル版は **2026-07-28 のみ**。2025 系の互換レーンは
持たない。この版が要求するリクエスト毎の `_meta` envelope を欠くリクエスト
(2025 系の `initialize` ハンドシェイクを含む)は、endpoint が提供する唯一の版を
名指しした unsupported-protocol-version エラーで返る。SDK は旧版向けの
ステートレスなフォールバックを用意しているが、意図して有効化していない(gh#224)。

理由はフォールバックが技術的に誤りだからではなく、保守されないフォールバックが
死にコード化するからである。畳む条件は観測可能(旧レーンへの到達がゼロになる)だが、
観測しに行く者がいない。破断はコードではなく別チャネル、すなわち minor リリースと
README 注意書きで吸収する。

ステートレス化の帰結:

- セッションが無い。各リクエストはそのリクエスト専用に組み立てたサーバー
インスタンスが処理する。セッション ID から解決するものは無く、`mcp-session-id`
も発行しない。
- ユーザー同一性は、処理中のリクエストが持つ OAuth props から取る。インスタンスの
フィールドではなく、ハンドラのリクエスト毎 auth context 経由で読む。
- かつて MCP を提供していた Durable Object(`RagMcpAgent` / `RagMcpAgentV2`)は
経路から外れた。過去の migration が名指しするクラスはスクリプト内に存在せねば
ならないという Cloudflare の制約のため、退役スタブとして export だけ残す。撤去には
`deleted_classes` migration が要る。実データを保持する `IssueStore` は無変更。

この版は本 repository の 2 成果物のあいだの私的な契約である。Worker へ到達する経路は
npx ブリッジ(`mcp-server/`)だけ——`server.json` は stdio トランスポートしか宣言
していない——ため、第三者クライアントが版を交渉することはなく、ブリッジ側は探索せず
版を pin する。ブリッジが Claude Desktop に向ける面は SDK v1 / 2025 系のまま。
そちらの移行は別 issue(gh#228)。

Worker は全員共有の 1 デプロイ、ブリッジは利用者ごとであるため、切替は非対称になる。
Worker は一斉に飛び、ブリッジはホストプロセスの再起動でようやく追随する。版を固定して
いる利用者は再起動しても復旧しない。この層の切り捨ては明示的に受け入れる。

### 2. Webhook Receiver

webhook receiver は GitHub event を near real time で取り込む。
Expand Down
39 changes: 39 additions & 0 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,45 @@ The check compares two axes: param names and, for enum params, their values —
a name-only comparison let the proxy `type` enum omit `wiki_doc` while the Worker
accepted it, so clients rejected the value before any request was sent (gh#181).

#### Protocol revision: 2026-07-28, single lane

The Worker serves MCP protocol revision **2026-07-28 only**. There is no
compatibility lane for the 2025 era: a request without the revision's
per-request `_meta` envelope — including a 2025 `initialize` handshake — is
answered with the unsupported-protocol-version error naming the one revision
the endpoint serves. The SDK offers a stateless fallback for the older era;
it is deliberately not enabled (gh#224).

The reason is not that a fallback would be technically wrong, but that an
unmaintained one becomes dead code. Its removal condition is observable (no
traffic reaches the old lane) yet nobody goes and observes it. The breakage is
absorbed on a different channel instead: a minor release plus a README notice.

Consequences of statelessness:

- No session. Each request is served by a server instance built for it alone,
so nothing is resolved from a session ID and `mcp-session-id` is not issued.
- User identity comes from the OAuth props of the request being served, read
through the handler's per-request auth context rather than from an instance
field.
- The Durable Objects that once served MCP (`RagMcpAgent`, `RagMcpAgentV2`) are
out of the serving path. Their classes remain exported as retired stubs
because Cloudflare requires every class named in a past migration to exist in
the script; removing them needs a `deleted_classes` migration. `IssueStore`,
which holds real data, is untouched.

The revision is a private contract between this repository's two artifacts. The
npx bridge (`mcp-server/`) is the only way to reach the Worker — `server.json`
declares stdio transport alone — so no third-party client negotiates with it,
and the bridge pins the revision instead of probing for it. The bridge's own
face toward Claude Desktop stays on SDK v1 / the 2025 era; that side is a
separate migration (gh#228).

Because the Worker is one shared deployment and each bridge is per user, the
flip is asymmetric: the Worker moves at once, while a bridge follows only when
its host process restarts. A user pinned to an older bridge version does not
recover by restarting. That cohort is explicitly cut off.

### 2. Webhook Receiver

The webhook receiver ingests GitHub events in near real time.
Expand Down
15 changes: 13 additions & 2 deletions mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@ Stdio MCP proxy that bridges local MCP clients (Claude Desktop, Claude Code, etc

This package is the **client-side proxy only**. The actual indexing pipeline (Vectorize + D1 FTS5 + Workers AI BGE-M3 + cross-encoder rerank) runs on the Worker. See the [main repository](https://github.com/Liplus-Project/github-rag-mcp) for architecture and self-hosting instructions.

## Breaking change: MCP protocol revision 2026-07-28

From this release the Worker serves **MCP protocol revision 2026-07-28 only**, with no compatibility lane for the previous revision.

- **Proxy versions older than this release stop working.** They open a session with `initialize`, which the Worker no longer answers. The failure is quiet: the proxy does not crash, it returns the protocol error as tool output text.
- **Restart Claude Desktop to pick up the new proxy.** `npx` resolves `@latest` at process start, so an already-running Claude Desktop keeps the copy it launched with however new the published version is. Quit it fully and reopen.
- **Pinning the proxy version leaves you stuck.** If your MCP client config pins a version older than this release, restarting does not help; remove the pin (or move it forward) first.

Nothing else changes for you: the proxy still speaks the same MCP dialect to Claude Desktop, and the same `search` tool with the same parameters.

## What this proxy does

- Speaks stdio MCP locally to your client.
- Forwards `tools/call` to the Worker's Streamable HTTP MCP endpoint (`/mcp`).
- Speaks stdio MCP locally to your client (SDK v1, unchanged by the revision above).
- Forwards `tools/call` to the Worker's `/mcp` endpoint over MCP protocol revision 2026-07-28. That side is stateless: no `initialize` handshake and no session id — each call carries the revision's per-request envelope.
- Handles OAuth 2.1 with PKCE against the Worker (browser-based localhost callback).
- Caches access and refresh tokens under `~/.github-rag-mcp/` (mode `0600`).

Expand Down Expand Up @@ -123,6 +133,7 @@ The browser callback never leaves your machine; the authorization code is delive
URI set does not cover the callback port selected for this authorization.
- **`OAuth callback timed out after 5 minutes`.** Re-invoke any tool to restart the flow.
- **`Failed to reach worker`.** Check that `RAG_WORKER_URL` is correct and reachable from your machine.
- **`Unsupported protocol version`, or a tool that answers with a protocol error instead of results.** The proxy predates the 2026-07-28 flip. Quit Claude Desktop fully and reopen so `npx` fetches the current version; if your config pins a version, move the pin forward first.
- **Stale credentials.** Remove `~/.github-rag-mcp/oauth-tokens.json` (and optionally `oauth-client.json`) and retry.

## Links
Expand Down
51 changes: 50 additions & 1 deletion mcp-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"pack:mcpb": "mcpb pack"
},
"dependencies": {
"@modelcontextprotocol/client": "^2.0.0",
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
Expand Down
118 changes: 40 additions & 78 deletions mcp-server/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,25 @@
* GitHub RAG MCP — Cloudflare Worker bridge
*
* Thin stdio MCP server that proxies tool calls to a remote
* Cloudflare Worker + Durable Object backend via Streamable HTTP.
* Authenticates via OAuth 2.1 with PKCE (localhost callback).
* Cloudflare Worker backend. Authenticates via OAuth 2.1 with PKCE
* (localhost callback).
*
* Tools are proxied to the Worker's MCP endpoint:
* search — unified hybrid search / time-ordered activity scan /
* inline doc content fetch via Vectorize + Workers AI
*
* The bridge has two independent protocol faces (issue #224):
*
* Claude Desktop -> bridge : SDK v1 stdio server, 2025-era. Unchanged.
* bridge -> Worker : SDK v2 client pinned to protocol revision
* 2026-07-28. Stateless — no `initialize`
* handshake and no `mcp-session-id`; every
* request carries the per-request `_meta`
* envelope the revision requires.
*
* The Worker's revision is a private contract between the two artifacts of
* this repository, so the Desktop face is not bound by it. Moving the Desktop
* face to SDK v2 is issue #228.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
Expand All @@ -24,6 +37,7 @@ import { join } from "node:path";
import { exec } from "node:child_process";
import { createRequire } from "node:module";
import { supportsRedirectUris } from "./oauth-client-registration.js";
import { createRemoteClient } from "./remote-client.js";
import { TOOLS } from "./tools.js";

const require = createRequire(import.meta.url);
Expand Down Expand Up @@ -328,87 +342,35 @@ async function getAccessToken() {
return _cachedTokens.access_token;
}

/** Build common headers with OAuth Bearer auth */
async function authHeaders(extra) {
const h = { ...extra };
const token = await getAccessToken();
if (token) h["Authorization"] = `Bearer ${token}`;
return h;
}

// ── Remote MCP Session (lazy, reused) ────────────────────────────────────────

let _sessionId = null;

async function getSessionId() {
if (_sessionId) return _sessionId;

const res = await fetch(`${WORKER_URL}/mcp`, {
method: "POST",
headers: await authHeaders({
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
}),
body: JSON.stringify({
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "github-rag-mcp-bridge", version: PACKAGE_VERSION },
},
id: "init",
}),
});

_sessionId = res.headers.get("mcp-session-id") || "";
return _sessionId;
}
// ── Remote MCP Client (lazy, reused) ─────────────────────────────────────────
// Construction and caching live in ./remote-client.js so they can be tested
// without importing this module (which connects the stdio transport on import).

const remote = createRemoteClient({
workerUrl: WORKER_URL,
clientVersion: PACKAGE_VERSION,
// The OAuth flow above stays the source of tokens; this only hands the
// current one over, and clears the cache when the Worker says it is stale so
// the next `token()` re-mints.
authProvider: {
token: () => getAccessToken(),
onUnauthorized: async () => {
_cachedTokens = null;
await getAccessToken();
},
},
});

async function callRemoteTool(name, args) {
const sessionId = await getSessionId();

const res = await fetch(`${WORKER_URL}/mcp`, {
method: "POST",
headers: await authHeaders({
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"mcp-session-id": sessionId,
}),
body: JSON.stringify({
jsonrpc: "2.0",
method: "tools/call",
params: { name, arguments: args },
id: crypto.randomUUID(),
}),
});

// 401 = token expired or revoked, re-authenticate and retry
if (res.status === 401) {
_cachedTokens = null;
_sessionId = null;
return callRemoteTool(name, args);
}

const text = await res.text();

// Streamable HTTP may return SSE format
const dataLine = text.split("\n").find((l) => l.startsWith("data: "));
const json = dataLine ? JSON.parse(dataLine.slice(6)) : JSON.parse(text);

if (json.error) {
// Session expired — retry once with a fresh session
if (json.error.code === -32600 || json.error.code === -32001) {
_sessionId = null;
return callRemoteTool(name, args);
}
return { content: [{ type: "text", text: JSON.stringify(json.error) }] };
}
// Resolve credentials first so an interactive-auth requirement surfaces as
// OAuthPendingError from here, where the caller already handles it, rather
// than from inside the transport wrapped as a network failure.
await getAccessToken();

return json.result;
return await remote.callTool(name, args);
}

// ── MCP Server Setup ─────────────────────────────────────────────────────────
// ── MCP Server Setup (Claude Desktop face — SDK v1, unchanged) ───────────────

const server = new Server(
{ name: "github-rag-mcp", version: PACKAGE_VERSION },
Expand Down
Loading
Loading