linear-mcp-wrapper
by hotaka052
README.md
# linear-mcp-wrapper
Linear の MCP サーバーへの接続を肩代わりする薄いサーバー。
呼ぶ側([tsuzuri](https://github.com/hotaka052/tsuzuri) の runner)は**このサービスの
URL だけ**を知っていればよく、Linear の資格情報を一切持たない、という形にするための repo。
```
before: runner ──MCP──> mcp.linear.app (runner が client_secret を持つ)
after: runner ──MCP──> このサービス ──MCP──> mcp.linear.app
```
## いまの状態: 置いただけ
**まだサーバーとして動かない。** tsuzuri から認証まわりのコードを退避させただけで、
プロキシ本体(`src/main.py`)はまだ無い。tsuzuri 側も**まだ何も消していない**ので、
そちらは今までどおり直接 Linear を見ている。
| ファイル | 出どころ |
| --- | --- |
| `src/auth.py` | tsuzuri の `runner/src/linear.py` を**そのままコピー**(1 文字も変えていない) |
| `src/config.py` | tsuzuri の `runner/src/config.py` の Linear の部分だけ |
| `src/logs.py` | tsuzuri の `runner/src/logs.py` から `emit` だけ |
| `env/master.yaml` | tsuzuri の `runner/env/master.yaml` の Linear の部分だけ |
### `src/auth.py` の docstring で、もう合っていない記述
そのままコピーした副作用。**中身の理屈は全部生きているが、参照先の名前だけが古い。**
- 「入口は intake.py、出口は writeback.py」— どちらも tsuzuri 側に残っている
- 「MCP サーバーへの接続ヘッダに使う(mcp_tools.build)。**唯一の呼び出し元。**」—
ここを呼ぶのは、これから書くプロキシ本体になる
## これから書くもの
1. **プロキシ本体(`src/main.py`)。** ここが本番。素通しの HTTP プロキシでは済まない:
- POST のレスポンスが JSON のことも SSE のこともある
(クライアントは `Accept: application/json, text/event-stream` で来る)
- 別に **GET で長寿命の SSE を張る**(サーバー→クライアント方向。切れたら再接続してくる)
- **`mcp-session-id` と `mcp-protocol-version` を両方向で素通しする**必要がある。
初回レスポンスで発行されたセッション ID を、クライアントは以降の全リクエストに付けてくる
- セッション終了は DELETE
- **バッファしないこと。** `await response.json()` で受けると SSE がそこで固まる。
`httpx.AsyncClient.stream()` + Starlette の `StreamingResponse` で流す
2. **呼ぶ側の認証。** クライアントから来る `Authorization` は Cloud Run の OIDC ID token
にして、ここで上流用のトークン(`auth.py`)に差し替える。
Cloud Run は `--no-allow-unauthenticated` で立てる
3. **ツールの絞り込み(あとで)。** `tools/list` のレスポンスから許可リストにない
ツールを落とす。いまは tsuzuri 側の `INTAKE_TOOL_FILTER` /
`WRITEBACK_TOOL_FILTER` がやっているので、動いてから移す
### 3 をやるときに知っておくこと
**上流のツールを全部そのまま渡すと、呼ぶ側のモデル呼び出しごと落ちる。**
Linear の MCP は 53 本公開していて、Vertex の function declaration が受け付けない
スキーマが混ざっていると `400 INVALID_ARGUMENT` になる(tsuzuri で実測)。
いま tsuzuri が使っているのは実質この 4 本だけ:
- 入口: `get_issue`, `list_comments`
- 出口: `save_comment`, `save_issue`
**入口と出口でリストを分けていること自体が権限の境界**になっている(一本にまとめると、
依頼を読むだけのエージェントが `delete_comment` を持つ)。ここに移すときも分けたまま
持ってくること。エンドポイントを分けるのが素直。
## 開発
```sh
uv sync
```
ローカルでは `LINEAR_API_KEY`(個人 API キー)を入れれば OAuth を通らずに済む。
Cloud Run では `LINEAR_CLIENT_ID` + `LINEAR_CLIENT_SECRET`(Secret Manager)を使う。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues