diff --git a/README.ja-JP.md b/README.ja-JP.md index fd7fbd19..64646db9 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -768,11 +768,14 @@ OCRは4層の優先度チェーンを使ってレビュールールを解決し | `llm.extra_headers` | string | カンマ区切りの `key=value` HTTPヘッダー | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | -| `mcp_servers..command` | string | MCPサーバーを起動するコマンド | -| `mcp_servers..args` | array | MCPサーバーのコマンドライン引数 | -| `mcp_servers..env` | array | 環境変数(`KEY=VALUE`形式) | +| `mcp_servers..type` | string | `stdio`(デフォルト)または `remote` | +| `mcp_servers..command` | string | MCPサーバーを起動するコマンド(stdio) | +| `mcp_servers..args` | array | MCPサーバーのコマンドライン引数(stdio) | +| `mcp_servers..env` | array | 環境変数(`KEY=VALUE`形式)(stdio) | +| `mcp_servers..url` | string | リモートMCPサーバーのURL(http/https) | +| `mcp_servers..headers` | object | リモートサーバーのHTTPヘッダー(`$ENV_VAR`展開対応) | | `mcp_servers..tools` | array | 許可するツール名(空の場合はすべてのツール) | -| `mcp_servers..setup` | string | サーバー起動前に実行するセットアップコマンド | +| `mcp_servers..setup` | string | サーバー起動前に実行するセットアップコマンド(stdio) | | `language` | string | 任意の言語名、例:`English`、`Chinese`(デフォルト:`English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -783,32 +786,42 @@ OCRは4層の優先度チェーンを使ってレビュールールを解決し ### MCPサーバー -Open Code Reviewは[Model Context Protocol (MCP)](https://modelcontextprotocol.io/)サーバーをサポートしており、レビューエージェントがstdioトランスポートを介してコードレビュー中に外部ツールを使用できます。 +Open Code Reviewは[Model Context Protocol (MCP)](https://modelcontextprotocol.io/)サーバーをサポートしており、レビューエージェントがコードレビュー中に外部ツールを使用できます。**stdio**(ローカルサブプロセス)と**remote**(Streamable HTTP)の2つのトランスポートに対応しています。 CLIからMCPサーバーを設定します: ```bash -# MCPサーバーを追加 +# Stdio MCPサーバー(デフォルト) ocr config set mcp_servers..command ocr config set mcp_servers..args '["arg1","arg2"]' ocr config set mcp_servers..env '["KEY=VALUE"]' ocr config set mcp_servers..tools '["tool_name"]' ocr config set mcp_servers..setup '' +# リモートMCPサーバー +ocr config set mcp_servers..type remote +ocr config set mcp_servers..url https://mcp.example.com/mcp +ocr config set mcp_servers..headers '{"Authorization":"Bearer $MCP_TOKEN"}' + # MCPサーバーを削除 ocr config unset mcp_servers. ``` | フィールド | 必須 | 説明 | |-----------|------|------| -| `command` | はい | MCPサーバーを起動する実行コマンド | +| `type` | いいえ | `stdio`(デフォルト)または `remote` | +| `command` | はい(stdio) | MCPサーバーを起動する実行コマンド | | `args` | いいえ | サーバーに渡すコマンドライン引数 | | `env` | いいえ | 環境変数(`KEY=VALUE`形式) | +| `url` | はい(remote) | リモートMCPサーバーのURL(http/https) | +| `headers` | いいえ | HTTPヘッダー(JSONオブジェクト)。値は実行時に`$ENV_VAR`が展開されます | | `tools` | いいえ | 許可するツール名。空の場合、サーバーのすべてのツールが利用可能 | | `setup` | いいえ | サーバー起動前に実行するシェルコマンド(例:インデックスの構築) | > **注意:** MCPツールの名前が組み込みツールと競合する場合、そのツールは警告付きでスキップされます。`setup`コマンドのタイムアウトは5分です。 +> **ヒント:** CLIでheadersを設定する際は、シェルが`$ENV_VAR`を展開しないよう**シングルクォート**を使用してください。値はOCRが接続時に展開します(設定保存時ではありません)。または`~/.opencodereview/config.json`を直接編集してください。 + **例:[CodeGraph](https://github.com/nicholasgasior/codegraph)を追加してコード構造分析を強化** ```bash @@ -818,6 +831,30 @@ ocr config set mcp_servers.codegraph.tools '["codegraph_explore"]' ocr config set mcp_servers.codegraph.setup 'codegraph init && codegraph index' ``` +**例:リモートMCPサーバーに接続** + +```bash +ocr config set mcp_servers.my-remote.type remote +ocr config set mcp_servers.my-remote.url 'https://mcp.example.com/mcp' +ocr config set mcp_servers.my-remote.headers '{"Authorization":"Bearer $MCP_TOKEN"}' +``` + +または`~/.opencodereview/config.json`で: + +```json +{ + "mcp_servers": { + "my-remote": { + "type": "remote", + "url": "https://mcp.example.com/mcp", + "headers": { + "Authorization": "Bearer $MCP_TOKEN" + } + } + } +} +``` + ### 環境変数 | 変数 | 用途 | diff --git a/README.ko-KR.md b/README.ko-KR.md index bd9da5c7..fe42e7de 100644 --- a/README.ko-KR.md +++ b/README.ko-KR.md @@ -726,11 +726,14 @@ Config file: `~/.opencodereview/config.json` | `llm.extra_headers` | string | 쉼표로 구분된 `key=value` HTTP 헤더 | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | -| `mcp_servers..command` | string | MCP 서버를 시작하는 명령어 | -| `mcp_servers..args` | array | MCP 서버의 커맨드라인 인수 | -| `mcp_servers..env` | array | 환경 변수 (`KEY=VALUE` 형식) | +| `mcp_servers..type` | string | `stdio` (기본값) 또는 `remote` | +| `mcp_servers..command` | string | MCP 서버를 시작하는 명령어 (stdio) | +| `mcp_servers..args` | array | MCP 서버의 커맨드라인 인수 (stdio) | +| `mcp_servers..env` | array | 환경 변수 (`KEY=VALUE` 형식) (stdio) | +| `mcp_servers..url` | string | 원격 MCP 서버 URL (http/https) | +| `mcp_servers..headers` | object | 원격 서버의 HTTP 헤더 (`$ENV_VAR` 확장 지원) | | `mcp_servers..tools` | array | 허용할 도구 이름 (비어 있으면 모든 도구 허용) | -| `mcp_servers..setup` | string | 서버 시작 전에 실행할 설정 명령어 | +| `mcp_servers..setup` | string | 서버 시작 전에 실행할 설정 명령어 (stdio) | | `language` | string | 임의의 언어 이름, 예: `English`, `Chinese` (기본값: `English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -741,32 +744,42 @@ Config file: `~/.opencodereview/config.json` ### MCP Server -Open Code Review는 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 서버를 지원하여 리뷰 에이전트가 stdio 전송을 통해 코드 리뷰 중에 외부 도구를 사용할 수 있습니다. +Open Code Review는 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 서버를 지원하여 리뷰 에이전트가 코드 리뷰 중에 외부 도구를 사용할 수 있습니다. **stdio** (로컬 서브프로세스)와 **remote** (Streamable HTTP) 두 가지 전송 방식을 지원합니다. CLI로 MCP 서버를 설정합니다: ```bash -# MCP 서버 추가 +# Stdio MCP 서버 (기본값) ocr config set mcp_servers..command ocr config set mcp_servers..args '["arg1","arg2"]' ocr config set mcp_servers..env '["KEY=VALUE"]' ocr config set mcp_servers..tools '["tool_name"]' ocr config set mcp_servers..setup '' +# 원격 MCP 서버 +ocr config set mcp_servers..type remote +ocr config set mcp_servers..url https://mcp.example.com/mcp +ocr config set mcp_servers..headers '{"Authorization":"Bearer $MCP_TOKEN"}' + # MCP 서버 삭제 ocr config unset mcp_servers. ``` | 필드 | 필수 | 설명 | |------|------|------| -| `command` | 예 | MCP 서버를 시작하는 실행 명령어 | +| `type` | 아니오 | `stdio` (기본값) 또는 `remote` | +| `command` | 예 (stdio) | MCP 서버를 시작하는 실행 명령어 | | `args` | 아니오 | 서버에 전달할 커맨드라인 인수 | | `env` | 아니오 | 환경 변수 (`KEY=VALUE` 형식) | +| `url` | 예 (remote) | 원격 MCP 서버의 URL (http/https) | +| `headers` | 아니오 | HTTP 헤더 (JSON 객체). 값은 런타임에 `$ENV_VAR`가 확장됩니다 | | `tools` | 아니오 | 허용할 도구 이름. 비어 있으면 서버의 모든 도구 사용 가능 | | `setup` | 아니오 | 서버 시작 전에 실행할 셸 명령어 (예: 인덱스 빌드) | > **참고:** MCP 도구의 이름이 내장 도구와 충돌하면 경고와 함께 건너뜁니다. `setup` 명령어의 타임아웃은 5분입니다. +> **팁:** CLI에서 headers를 설정할 때 셸이 `$ENV_VAR`를 확장하지 않도록 **작은따옴표**를 사용하세요. 값은 설정 저장 시가 아닌 OCR 연결 시에 확장됩니다. 또는 `~/.opencodereview/config.json`을 직접 편집하세요. + **예시: [CodeGraph](https://github.com/nicholasgasior/codegraph)를 추가하여 코드 구조 분석 강화** ```bash @@ -776,6 +789,30 @@ ocr config set mcp_servers.codegraph.tools '["codegraph_explore"]' ocr config set mcp_servers.codegraph.setup 'codegraph init && codegraph index' ``` +**예시: 원격 MCP 서버에 연결** + +```bash +ocr config set mcp_servers.my-remote.type remote +ocr config set mcp_servers.my-remote.url 'https://mcp.example.com/mcp' +ocr config set mcp_servers.my-remote.headers '{"Authorization":"Bearer $MCP_TOKEN"}' +``` + +또는 `~/.opencodereview/config.json`에서: + +```json +{ + "mcp_servers": { + "my-remote": { + "type": "remote", + "url": "https://mcp.example.com/mcp", + "headers": { + "Authorization": "Bearer $MCP_TOKEN" + } + } + } +} +``` + ### Environment Variables | Variable | Purpose | diff --git a/README.md b/README.md index 7d8412fd..84a5e544 100644 --- a/README.md +++ b/README.md @@ -775,11 +775,14 @@ Config file: `~/.opencodereview/config.json` | `llm.extra_headers` | string | Comma-separated `key=value` HTTP headers | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | -| `mcp_servers..command` | string | Command to start the MCP server | -| `mcp_servers..args` | array | Command-line arguments for the MCP server | -| `mcp_servers..env` | array | Environment variables in `KEY=VALUE` format | +| `mcp_servers..type` | string | `stdio` (default) or `remote` | +| `mcp_servers..command` | string | Command to start the MCP server (stdio) | +| `mcp_servers..args` | array | Command-line arguments for the MCP server (stdio) | +| `mcp_servers..env` | array | Environment variables in `KEY=VALUE` format (stdio) | +| `mcp_servers..url` | string | URL for remote MCP server (http/https) | +| `mcp_servers..headers` | object | HTTP headers for remote server (supports `$ENV_VAR` expansion) | | `mcp_servers..tools` | array | Allowed tool names (empty = all tools) | -| `mcp_servers..setup` | string | Setup command to run before starting the server | +| `mcp_servers..setup` | string | Setup command to run before starting the server (stdio) | | `language` | string | Any language name, e.g. `English`, `Chinese` (default: `English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -790,32 +793,42 @@ Environment variables take precedence over the config file. ### MCP Server -Open Code Review supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, allowing the review agent to use external tools during code review via the stdio transport. +Open Code Review supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, allowing the review agent to use external tools during code review. Two transports are supported: **stdio** (local subprocess) and **remote** (Streamable HTTP). Configure MCP servers via the CLI: ```bash -# Add an MCP server +# Stdio MCP server (default) ocr config set mcp_servers..command ocr config set mcp_servers..args '["arg1","arg2"]' ocr config set mcp_servers..env '["KEY=VALUE"]' ocr config set mcp_servers..tools '["tool_name"]' ocr config set mcp_servers..setup '' +# Remote MCP server +ocr config set mcp_servers..type remote +ocr config set mcp_servers..url https://mcp.example.com/mcp +ocr config set mcp_servers..headers '{"Authorization":"Bearer $MCP_TOKEN"}' + # Delete an MCP server ocr config unset mcp_servers. ``` | Field | Required | Description | |-------|----------|-------------| -| `command` | Yes | The executable command to start the MCP server | +| `type` | No | `stdio` (default) or `remote` | +| `command` | Yes (stdio) | The executable command to start the MCP server | | `args` | No | Command-line arguments passed to the server | | `env` | No | Environment variables in `KEY=VALUE` format | +| `url` | Yes (remote) | URL of the remote MCP server (http/https) | +| `headers` | No | HTTP headers as a JSON object; values support `$ENV_VAR` expansion at runtime | | `tools` | No | Allowed tool names; if empty, all tools from the server are available | | `setup` | No | A shell command to run before starting the server (e.g. build an index) | > **Note:** If an MCP tool's name conflicts with a built-in tool, it will be skipped with a warning. The `setup` command has a 5-minute timeout. +> **Tip:** When setting headers via CLI, use **single quotes** to prevent the shell from expanding `$ENV_VAR` references. The values are expanded at runtime by OCR, not at config-save time. Alternatively, edit `~/.opencodereview/config.json` directly. + **Example: Add [CodeGraph](https://github.com/nicholasgasior/codegraph) for code structure analysis** ```bash @@ -825,6 +838,30 @@ ocr config set mcp_servers.codegraph.tools '["codegraph_explore"]' ocr config set mcp_servers.codegraph.setup 'codegraph init && codegraph index' ``` +**Example: Connect to a remote MCP server** + +```bash +ocr config set mcp_servers.my-remote.type remote +ocr config set mcp_servers.my-remote.url 'https://mcp.example.com/mcp' +ocr config set mcp_servers.my-remote.headers '{"Authorization":"Bearer $MCP_TOKEN"}' +``` + +Or in `~/.opencodereview/config.json`: + +```json +{ + "mcp_servers": { + "my-remote": { + "type": "remote", + "url": "https://mcp.example.com/mcp", + "headers": { + "Authorization": "Bearer $MCP_TOKEN" + } + } + } +} +``` + ### Environment Variables | Variable | Purpose | diff --git a/README.ru-RU.md b/README.ru-RU.md index 84f2cf34..df688d7b 100644 --- a/README.ru-RU.md +++ b/README.ru-RU.md @@ -772,11 +772,14 @@ OCR разрешает правила ревью по цепочке приор | `llm.extra_headers` | string | HTTP-заголовки `key=value` через запятую | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | -| `mcp_servers..command` | string | Команда для запуска MCP-сервера | -| `mcp_servers..args` | array | Аргументы командной строки для MCP-сервера | -| `mcp_servers..env` | array | Переменные окружения в формате `KEY=VALUE` | +| `mcp_servers..type` | string | `stdio` (по умолчанию) или `remote` | +| `mcp_servers..command` | string | Команда для запуска MCP-сервера (stdio) | +| `mcp_servers..args` | array | Аргументы командной строки для MCP-сервера (stdio) | +| `mcp_servers..env` | array | Переменные окружения в формате `KEY=VALUE` (stdio) | +| `mcp_servers..url` | string | URL удалённого MCP-сервера (http/https) | +| `mcp_servers..headers` | object | HTTP-заголовки для удалённого сервера (поддержка `$ENV_VAR`) | | `mcp_servers..tools` | array | Разрешённые имена инструментов (пусто = все инструменты) | -| `mcp_servers..setup` | string | Команда настройки перед запуском сервера | +| `mcp_servers..setup` | string | Команда настройки перед запуском сервера (stdio) | | `language` | string | Любое название языка, например `English`, `Chinese` (по умолчанию: `English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -787,32 +790,42 @@ OCR разрешает правила ревью по цепочке приор ### MCP-сервер -Open Code Review поддерживает серверы [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), позволяя агенту ревью использовать внешние инструменты во время проверки кода через stdio-транспорт. +Open Code Review поддерживает серверы [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), позволяя агенту ревью использовать внешние инструменты во время проверки кода. Поддерживаются два транспорта: **stdio** (локальный подпроцесс) и **remote** (Streamable HTTP). Настройка MCP-серверов через CLI: ```bash -# Добавить MCP-сервер +# Stdio MCP-сервер (по умолчанию) ocr config set mcp_servers..command ocr config set mcp_servers..args '["arg1","arg2"]' ocr config set mcp_servers..env '["KEY=VALUE"]' ocr config set mcp_servers..tools '["tool_name"]' ocr config set mcp_servers..setup '' +# Удалённый MCP-сервер +ocr config set mcp_servers..type remote +ocr config set mcp_servers..url https://mcp.example.com/mcp +ocr config set mcp_servers..headers '{"Authorization":"Bearer $MCP_TOKEN"}' + # Удалить MCP-сервер ocr config unset mcp_servers. ``` | Поле | Обязательно | Описание | |------|-------------|----------| -| `command` | Да | Исполняемая команда для запуска MCP-сервера | +| `type` | Нет | `stdio` (по умолчанию) или `remote` | +| `command` | Да (stdio) | Исполняемая команда для запуска MCP-сервера | | `args` | Нет | Аргументы командной строки для сервера | | `env` | Нет | Переменные окружения в формате `KEY=VALUE` | +| `url` | Да (remote) | URL удалённого MCP-сервера (http/https) | +| `headers` | Нет | HTTP-заголовки в виде JSON-объекта; значения поддерживают подстановку `$ENV_VAR` во время выполнения | | `tools` | Нет | Разрешённые имена инструментов; если пусто — доступны все инструменты сервера | | `setup` | Нет | Shell-команда для выполнения перед запуском сервера (например, построение индекса) | > **Примечание:** Если имя MCP-инструмента конфликтует со встроенным инструментом, он будет пропущен с предупреждением. Таймаут команды `setup` составляет 5 минут. +> **Совет:** При установке headers через CLI используйте **одинарные кавычки**, чтобы shell не раскрыл ссылки `$ENV_VAR`. Значения раскрываются OCR при подключении, а не при сохранении конфигурации. Также можно редактировать `~/.opencodereview/config.json` напрямую. + **Пример: добавление [CodeGraph](https://github.com/nicholasgasior/codegraph) для усиления анализа структуры кода** ```bash @@ -822,6 +835,30 @@ ocr config set mcp_servers.codegraph.tools '["codegraph_explore"]' ocr config set mcp_servers.codegraph.setup 'codegraph init && codegraph index' ``` +**Пример: подключение к удалённому MCP-серверу** + +```bash +ocr config set mcp_servers.my-remote.type remote +ocr config set mcp_servers.my-remote.url 'https://mcp.example.com/mcp' +ocr config set mcp_servers.my-remote.headers '{"Authorization":"Bearer $MCP_TOKEN"}' +``` + +Или в `~/.opencodereview/config.json`: + +```json +{ + "mcp_servers": { + "my-remote": { + "type": "remote", + "url": "https://mcp.example.com/mcp", + "headers": { + "Authorization": "Bearer $MCP_TOKEN" + } + } + } +} +``` + ### Переменные окружения | Переменная | Назначение | diff --git a/README.zh-CN.md b/README.zh-CN.md index 579eabd3..da9765d8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -757,11 +757,14 @@ OCR 通过四层优先级链解析评审规则。每层采用首次匹配原则 | `llm.extra_headers` | string | 逗号分隔的 `key=value` HTTP 头 | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | -| `mcp_servers..command` | string | 启动 MCP 服务器的命令 | -| `mcp_servers..args` | array | MCP 服务器的命令行参数 | -| `mcp_servers..env` | array | 环境变量,`KEY=VALUE` 格式 | +| `mcp_servers..type` | string | `stdio`(默认)或 `remote` | +| `mcp_servers..command` | string | 启动 MCP 服务器的命令(stdio) | +| `mcp_servers..args` | array | MCP 服务器的命令行参数(stdio) | +| `mcp_servers..env` | array | 环境变量,`KEY=VALUE` 格式(stdio) | +| `mcp_servers..url` | string | 远程 MCP 服务器的 URL(http/https) | +| `mcp_servers..headers` | object | 远程服务器的 HTTP 头(支持 `$ENV_VAR` 展开) | | `mcp_servers..tools` | array | 允许使用的工具名称(为空则允许所有工具) | -| `mcp_servers..setup` | string | 启动服务器前运行的初始化命令 | +| `mcp_servers..setup` | string | 启动服务器前运行的初始化命令(stdio) | | `language` | string | 任意语言名称,例如 `English`、`Chinese`(默认:`English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -772,32 +775,42 @@ OCR 通过四层优先级链解析评审规则。每层采用首次匹配原则 ### MCP Server -Open Code Review 支持 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 服务器,允许评审 Agent 在代码评审过程中通过 stdio 传输协议调用外部工具。 +Open Code Review 支持 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 服务器,允许评审 Agent 在代码评审过程中调用外部工具。支持两种传输方式:**stdio**(本地子进程)和 **remote**(Streamable HTTP)。 通过 CLI 配置 MCP 服务器: ```bash -# 添加 MCP 服务器 +# Stdio MCP 服务器(默认) ocr config set mcp_servers..command ocr config set mcp_servers..args '["arg1","arg2"]' ocr config set mcp_servers..env '["KEY=VALUE"]' ocr config set mcp_servers..tools '["tool_name"]' ocr config set mcp_servers..setup '' +# 远程 MCP 服务器 +ocr config set mcp_servers..type remote +ocr config set mcp_servers..url https://mcp.example.com/mcp +ocr config set mcp_servers..headers '{"Authorization":"Bearer $MCP_TOKEN"}' + # 删除 MCP 服务器 ocr config unset mcp_servers. ``` | 字段 | 必填 | 说明 | |------|------|------| -| `command` | 是 | 启动 MCP 服务器的可执行命令 | +| `type` | 否 | `stdio`(默认)或 `remote` | +| `command` | 是(stdio) | 启动 MCP 服务器的可执行命令 | | `args` | 否 | 传递给服务器的命令行参数 | | `env` | 否 | 环境变量,`KEY=VALUE` 格式 | +| `url` | 是(remote) | 远程 MCP 服务器的 URL(http/https) | +| `headers` | 否 | HTTP 头,JSON 对象格式;值支持运行时 `$ENV_VAR` 展开 | | `tools` | 否 | 允许使用的工具名称;为空则服务器的所有工具均可用 | | `setup` | 否 | 启动服务器前运行的 shell 命令(例如构建索引) | > **注意:** 如果 MCP 工具的名称与内置工具冲突,该工具将被跳过并输出警告。`setup` 命令的超时时间为 5 分钟。 +> **提示:** 通过 CLI 设置 headers 时,请使用**单引号**以避免 shell 提前展开 `$ENV_VAR` 引用。OCR 会在连接时展开这些值,而不是在保存配置时。也可以直接编辑 `~/.opencodereview/config.json`。 + **示例:添加 [CodeGraph](https://github.com/nicholasgasior/codegraph) 增强代码结构分析能力** ```bash @@ -807,6 +820,30 @@ ocr config set mcp_servers.codegraph.tools '["codegraph_explore"]' ocr config set mcp_servers.codegraph.setup 'codegraph init && codegraph index' ``` +**示例:连接远程 MCP 服务器** + +```bash +ocr config set mcp_servers.my-remote.type remote +ocr config set mcp_servers.my-remote.url 'https://mcp.example.com/mcp' +ocr config set mcp_servers.my-remote.headers '{"Authorization":"Bearer $MCP_TOKEN"}' +``` + +或在 `~/.opencodereview/config.json` 中: + +```json +{ + "mcp_servers": { + "my-remote": { + "type": "remote", + "url": "https://mcp.example.com/mcp", + "headers": { + "Authorization": "Bearer $MCP_TOKEN" + } + } + } +} +``` + ### 环境变量 | 变量 | 用途 | diff --git a/cmd/opencodereview/config_cmd.go b/cmd/opencodereview/config_cmd.go index a7c13413..fe1ec3f2 100644 --- a/cmd/opencodereview/config_cmd.go +++ b/cmd/opencodereview/config_cmd.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "net/url" "os" "path/filepath" "strconv" @@ -198,13 +199,17 @@ type ProviderEntry struct { ExtraHeaders map[string]string `json:"extra_headers,omitempty"` } -// MCPServerConfig holds configuration for a single MCP server (stdio transport). +// MCPServerConfig holds configuration for a single MCP server. +// Type "stdio" (default) uses a subprocess; type "remote" uses Streamable HTTP. type MCPServerConfig struct { - Command string `json:"command"` - Args []string `json:"args,omitempty"` - Env []string `json:"env,omitempty"` - Tools []string `json:"tools,omitempty"` - Setup string `json:"setup,omitempty"` + Type string `json:"type,omitempty"` // "stdio" (default) or "remote" + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + Env []string `json:"env,omitempty"` + URL string `json:"url,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Tools []string `json:"tools,omitempty"` + Setup string `json:"setup,omitempty"` } // Config represents the user-level configuration file (~/.opencodereview/config.json). @@ -374,7 +379,7 @@ func setConfigValue(cfg *Config, key, value string) error { } cfg.Llm.ExtraBody = m default: - return fmt.Errorf("unknown config key: %s\nSupported keys: provider, model, providers.., custom_providers.., mcp_servers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, llm.extra_headers, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging\nProvider fields: api_key, url, protocol, model, models, auth_header, extra_body, extra_headers\nMCP server fields: command, args, env, tools, setup", key) + return fmt.Errorf("unknown config key: %s\nSupported keys: provider, model, providers.., custom_providers.., mcp_servers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, llm.extra_headers, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging\nProvider fields: api_key, url, protocol, model, models, auth_header, extra_body, extra_headers\nMCP server fields: type, command, args, env, url, headers, tools, setup", key) } return nil } @@ -539,6 +544,11 @@ func setMCPServerValue(cfg *Config, key, value string) error { entry := cfg.MCPServers[name] switch field { + case "type": + if value != "stdio" && value != "remote" { + return fmt.Errorf("invalid MCP server type %q: must be \"stdio\" or \"remote\"", value) + } + entry.Type = value case "command": if value == "" { return fmt.Errorf("MCP server command cannot be empty") @@ -562,6 +572,27 @@ func setMCPServerValue(cfg *Config, key, value string) error { } } entry.Env = env + case "url": + if value == "" { + return fmt.Errorf("MCP server URL cannot be empty") + } + parsed, err := url.Parse(value) + if err != nil { + return fmt.Errorf("invalid MCP server URL %q: %w", value, err) + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return fmt.Errorf("MCP server URL must use http or https scheme, got %q", parsed.Scheme) + } + if parsed.Host == "" { + return fmt.Errorf("MCP server URL %q must include a host", value) + } + entry.URL = value + case "headers": + parsed, err := parseMCPHeaders(value) + if err != nil { + return fmt.Errorf("invalid headers for %s: %w", key, err) + } + entry.Headers = parsed case "tools": var tools []string if err := json.Unmarshal([]byte(value), &tools); err != nil { @@ -583,13 +614,31 @@ func setMCPServerValue(cfg *Config, key, value string) error { case "setup": entry.Setup = value default: - return fmt.Errorf("unknown MCP server field %q: supported fields are command, args, env, tools, setup", field) + return fmt.Errorf("unknown MCP server field %q: supported fields are type, command, args, env, url, headers, tools, setup", field) } cfg.MCPServers[name] = entry return nil } +// parseMCPHeaders parses a JSON object of header key-value pairs. +// Example: {"Authorization": "Bearer $TOKEN", "X-Custom": "value"} +func parseMCPHeaders(value string) (map[string]string, error) { + var m map[string]string + if err := json.Unmarshal([]byte(value), &m); err != nil { + return nil, fmt.Errorf("expected JSON object: %w", err) + } + for k, v := range m { + if k == "" { + return nil, fmt.Errorf("header name must not be empty") + } + if v == "" { + return nil, fmt.Errorf("header value for %q must not be empty", k) + } + } + return m, nil +} + func (c *Config) ensureTelemetry() { if c.Telemetry == nil { c.Telemetry = &TelemetryConfig{} diff --git a/cmd/opencodereview/config_cmd_test.go b/cmd/opencodereview/config_cmd_test.go index 8c7de98d..a2016804 100644 --- a/cmd/opencodereview/config_cmd_test.go +++ b/cmd/opencodereview/config_cmd_test.go @@ -1040,3 +1040,79 @@ func TestEnsureModelInList(t *testing.T) { t.Errorf("new model should append: got %v, want %v", got, want) } } + +func TestSetMCPServerValue_Type(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.type", "remote"); err != nil { + t.Fatalf("setMCPServerValue: %v", err) + } + if cfg.MCPServers["gh"].Type != "remote" { + t.Errorf("Type = %q, want %q", cfg.MCPServers["gh"].Type, "remote") + } +} + +func TestSetMCPServerValue_TypeInvalid(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.type", "invalid"); err == nil { + t.Fatal("expected error for invalid type, got nil") + } +} + +func TestSetMCPServerValue_URL(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.url", "https://api.example.com/mcp"); err != nil { + t.Fatalf("setMCPServerValue: %v", err) + } + if cfg.MCPServers["gh"].URL != "https://api.example.com/mcp" { + t.Errorf("URL = %q, want %q", cfg.MCPServers["gh"].URL, "https://api.example.com/mcp") + } +} + +func TestSetMCPServerValue_URLEmpty(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.url", ""); err == nil { + t.Fatal("expected error for empty URL, got nil") + } +} + +func TestSetMCPServerValue_URLInvalidScheme(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.url", "ftp://example.com/mcp"); err == nil { + t.Fatal("expected error for non-http scheme, got nil") + } +} + +func TestSetMCPServerValue_Headers(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.headers", `{"Authorization":"Bearer $TOKEN","X-Custom":"val"}`); err != nil { + t.Fatalf("setMCPServerValue: %v", err) + } + h := cfg.MCPServers["gh"].Headers + if h["Authorization"] != "Bearer $TOKEN" { + t.Errorf("Authorization = %q, want %q", h["Authorization"], "Bearer $TOKEN") + } + if h["X-Custom"] != "val" { + t.Errorf("X-Custom = %q, want %q", h["X-Custom"], "val") + } +} + +func TestSetMCPServerValue_URLNoHost(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.url", "http://"); err == nil { + t.Fatal("expected error for URL without host, got nil") + } +} + +func TestSetMCPServerValue_HeadersInvalidJSON(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.headers", "not-json"); err == nil { + t.Fatal("expected error for invalid JSON, got nil") + } +} + +func TestSetMCPServerValue_HeadersEmptyValue(t *testing.T) { + cfg := &Config{} + if err := setMCPServerValue(cfg, "mcp_servers.gh.headers", `{"Authorization":""}`); err == nil { + t.Fatal("expected error for empty header value, got nil") + } +} diff --git a/cmd/opencodereview/flags.go b/cmd/opencodereview/flags.go index 1f72bb1f..602c5587 100644 --- a/cmd/opencodereview/flags.go +++ b/cmd/opencodereview/flags.go @@ -324,6 +324,11 @@ Examples: ocr config set mcp_servers.codegraph.args '["-y","@anthropic/codegraph-mcp"]' ocr config set mcp_servers.codegraph.env '["CODEGRAPH_TOKEN=xxx"]' + # Remote MCP server (Streamable HTTP transport) + ocr config set mcp_servers.remote-srv.type remote + ocr config set mcp_servers.remote-srv.url https://mcp.example.com/mcp + ocr config set mcp_servers.remote-srv.headers '{"Authorization":"Bearer $MCP_TOKEN"}' + # Delete an MCP server ocr config unset mcp_servers.codegraph @@ -338,5 +343,5 @@ Examples: Supported keys: provider, model, providers.., custom_providers.., mcp_servers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, llm.extra_headers, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging Provider fields: api_key, url, protocol, model, models, auth_header, extra_body, extra_headers -MCP server fields: command, args, env, tools, setup`) +MCP server fields: type, command, args, env, url, headers, tools, setup`) } diff --git a/cmd/opencodereview/review_cmd.go b/cmd/opencodereview/review_cmd.go index a0b94835..63d78a68 100644 --- a/cmd/opencodereview/review_cmd.go +++ b/cmd/opencodereview/review_cmd.go @@ -274,6 +274,26 @@ func initMCPClients(ctx context.Context, cfg *Config, tools *tool.Registry, repo var clients []*mcp.Client for _, name := range mcpNames { serverCfg := cfg.MCPServers[name] + + isRemote := serverCfg.Type == "remote" + + if isRemote { + if serverCfg.URL == "" { + fmt.Fprintf(os.Stderr, "[ocr] WARNING: remote MCP server %q has no URL configured, skipping\n", name) + continue + } + initCtx, initCancel := context.WithTimeout(ctx, 30*time.Second) + mc, err := mcp.NewRemoteClient(initCtx, name, serverCfg.URL, serverCfg.Headers, version) + initCancel() + if err != nil { + fmt.Fprintf(os.Stderr, "[ocr] WARNING: failed to connect to remote MCP server %q: %v\n", name, err) + continue + } + clients = append(clients, mc) + mcp.RegisterAll(tools, mc, serverCfg.Tools) + continue + } + if serverCfg.Command == "" { fmt.Fprintf(os.Stderr, "[ocr] WARNING: MCP server %q has no command configured, skipping\n", name) continue diff --git a/internal/mcp/client.go b/internal/mcp/client.go index b06d7cde..508353fa 100644 --- a/internal/mcp/client.go +++ b/internal/mcp/client.go @@ -3,6 +3,8 @@ package mcp import ( "context" "fmt" + "io" + "net/http" "os" "os/exec" "strings" @@ -10,15 +12,15 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -// Client wraps a single MCP server connection via stdio transport. +// Client wraps a single MCP server connection. type Client struct { name string session *mcp.ClientSession tools []*mcp.Tool } -// NewClient starts an MCP server subprocess, initializes the connection, -// and caches the list of available tools. The context governs the +// NewClient starts an MCP server subprocess (stdio transport), initializes the +// connection, and caches the list of available tools. The context governs the // initialization timeout (Connect + ListTools), NOT the subprocess // lifetime — the subprocess stays alive until Close is called. // When dir is non-empty, the subprocess runs with that working directory. @@ -60,6 +62,92 @@ func NewClient(ctx context.Context, name, command string, args, env []string, di }, nil } +// NewRemoteClient connects to a remote MCP server via Streamable HTTP transport. +// Header values may contain $ENV_VAR references which are expanded at runtime. +// Returns an error if any header value expands to an empty string. +func NewRemoteClient(ctx context.Context, name, url string, headers map[string]string, version string) (*Client, error) { + var expanded map[string]string + if len(headers) > 0 { + expanded = make(map[string]string, len(headers)) + for k, v := range headers { + expanded[k] = os.Expand(v, os.Getenv) + if expanded[k] == "" { + return nil, fmt.Errorf("MCP server %q header %q expanded to empty value — check your environment variables", name, k) + } + } + } + httpClient := &http.Client{ + Transport: &headerTransport{ + base: http.DefaultTransport, + headers: expanded, + serverName: name, + }, + } + + client := mcp.NewClient( + &mcp.Implementation{Name: "open-code-review", Version: version}, + nil, + ) + + transport := &mcp.StreamableClientTransport{ + Endpoint: url, + HTTPClient: httpClient, + } + session, err := client.Connect(ctx, transport, nil) + if err != nil { + return nil, fmt.Errorf("connect to remote MCP server %q at %s: %w", name, url, err) + } + + var success bool + defer func() { + if !success { + session.Close() + } + }() + + toolsResult, err := session.ListTools(ctx, nil) + if err != nil { + return nil, fmt.Errorf("list tools from remote MCP server %q: %w", name, err) + } + + success = true + return &Client{ + name: name, + session: session, + tools: toolsResult.Tools, + }, nil +} + +// headerTransport injects custom headers into every HTTP request and surfaces +// clear authentication errors for 401/403 responses. +type headerTransport struct { + base http.RoundTripper + headers map[string]string + serverName string +} + +func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) { + cloned := req.Clone(req.Context()) + for k, v := range t.headers { + cloned.Header.Set(k, v) + } + resp, err := t.base.RoundTrip(cloned) + if err != nil { + return nil, err + } + switch resp.StatusCode { + case http.StatusUnauthorized: + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + return nil, fmt.Errorf("remote MCP server %q returned HTTP 401 Unauthorized — check your token/header configuration", t.serverName) + case http.StatusForbidden: + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + return nil, fmt.Errorf("remote MCP server %q returned HTTP 403 Forbidden — your credentials may lack required permissions", t.serverName) + } + return resp, nil +} + func (c *Client) Name() string { return c.name } func (c *Client) Tools() []*mcp.Tool { return c.tools } diff --git a/internal/mcp/client_test.go b/internal/mcp/client_test.go index c5f04da9..6d3f3a37 100644 --- a/internal/mcp/client_test.go +++ b/internal/mcp/client_test.go @@ -3,11 +3,89 @@ package mcp import ( "context" "encoding/json" + "net/http" + "net/http/httptest" + "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" ) +func TestHeaderTransport(t *testing.T) { + headers := map[string]string{ + "Authorization": "Bearer test-token", + "X-Custom": "custom-value", + } + transport := &headerTransport{ + base: http.DefaultTransport, + headers: headers, + serverName: "test-server", + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer test-token" { + t.Errorf("Authorization = %q, want %q", got, "Bearer test-token") + } + if got := r.Header.Get("X-Custom"); got != "custom-value" { + t.Errorf("X-Custom = %q, want %q", got, "custom-value") + } + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + client := &http.Client{Transport: transport} + resp, err := client.Get(ts.URL) + if err != nil { + t.Fatalf("GET: %v", err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("status = %d, want 200", resp.StatusCode) + } +} + +func TestHeaderTransport_401(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + })) + defer ts.Close() + + transport := &headerTransport{ + base: http.DefaultTransport, + headers: map[string]string{"Authorization": "Bearer bad-token"}, + serverName: "auth-server", + } + client := &http.Client{Transport: transport} + _, err := client.Get(ts.URL) + if err == nil { + t.Fatal("expected error for 401, got nil") + } + if got := err.Error(); !strings.Contains(got, "401 Unauthorized") || !strings.Contains(got, "auth-server") { + t.Errorf("error = %q, want mention of 401 and server name", got) + } +} + +func TestHeaderTransport_403(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + })) + defer ts.Close() + + transport := &headerTransport{ + base: http.DefaultTransport, + headers: map[string]string{"Authorization": "Bearer limited-token"}, + serverName: "perm-server", + } + client := &http.Client{Transport: transport} + _, err := client.Get(ts.URL) + if err == nil { + t.Fatal("expected error for 403, got nil") + } + if got := err.Error(); !strings.Contains(got, "403 Forbidden") || !strings.Contains(got, "perm-server") { + t.Errorf("error = %q, want mention of 403 and server name", got) + } +} + func TestContentToText_SingleText(t *testing.T) { contents := []mcp.Content{ &mcp.TextContent{Text: "hello"},