diff --git a/docs/usage-guide.md b/docs/usage-guide.md index bb55cb526..21f9b0e5d 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -1343,7 +1343,7 @@ TeamAI removes a bare Copilot entry beside `mcpServers` only when its ownership An HTTP local-agent update keeps the existing JSON MCP ownership record until the config write succeeds. If saving the new record then fails, it restores the previous config. `uninstall_mcp` removes the entry before dropping its ownership record; a failed config write or an unreadable config keeps that record for a retry, and a failed manifest write restores the entry. A failed MCP reconcile restores each config it wrote before saving ownership, including a file shared by multiple tools. A restoration failure reports both errors and the affected files: repair the config and ownership record before retrying. Git protection remains while a credential is still present. -Copilot uses its native `mcpServers` schema: `stdio` becomes `type: "local"`, remote transports keep `http` or `sse`, and every managed entry gets the required `tools: ["*"]` allowlist. TeamAI honors `COPILOT_HOME`; project configuration uses Copilot CLI's documented `.github/mcp.json` repository location. See [Adding MCP servers for GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers). Codex supports `stdio` and `http`; `sse` is skipped. Qoder supports the Claude-compatible `mcpServers` format in its scope-specific `.qoder/settings.json`. Kiro supports the same `mcpServers` format in its dedicated, mcpServers-only `.kiro/settings/mcp.json` (see [Kiro's MCP configuration docs](https://kiro.dev/docs/mcp/configuration/)). OpenCode supports `stdio` (written as its `type:"local"` shape) and `http` (`type:"remote"`); `sse` is skipped, and its servers live under the `mcp` key of the shared `opencode.json`. Ownership is tracked in `~/.teamai/managed-mcp.json` — hand-added servers are left alone; name collisions skip unless `--force`. +Copilot uses its native `mcpServers` schema: `stdio` becomes `type: "local"`, remote transports keep `http` or `sse`, and every managed entry gets the required `tools: ["*"]` allowlist. TeamAI honors `COPILOT_HOME`; project configuration uses Copilot CLI's documented `.github/mcp.json` repository location. See [Adding MCP servers for GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers). Codex supports `stdio` and `http`; `sse` is skipped. Qoder supports the Claude-compatible `mcpServers` format in its scope-specific `.qoder/settings.json`. Kiro supports the same `mcpServers` format in its dedicated, mcpServers-only `.kiro/settings/mcp.json` (see [Kiro's MCP configuration docs](https://kiro.dev/docs/mcp/configuration/)). OpenCode supports `stdio` (written as its `type:"local"` shape), `http`, and `sse` (both `type:"remote"`, negotiated by its client); its servers live under the `mcp` key of the shared `opencode.json`. Ownership is tracked in `~/.teamai/managed-mcp.json` — hand-added servers are left alone; name collisions skip unless `--force`. **Secrets.** Write `${VAR}`, never a literal, in `mcp.yaml`. A key the team declares in `env/secrets.yaml` resolves from your value for this team (`teamai env set`), then your value for the machine (`teamai env set --global`), then your own environment, which leaves out values a teamai `env.sh` exported (see [Team secrets](designs/team-secrets.md#resolution)). Any other variable resolves from your value for this team (`teamai env set KEY`), then from the team env variables this directory receives (`env/env.yaml` and the active `env//env.yaml`); the environment fills only a key the team sets nothing for, and no longer overrides a team variable (see [Team secrets](designs/team-secrets.md#variables)). An interactive `pull` and `teamai doctor` say when your export differs from the team's value and is ignored. Unresolved variables skip the server with a hint. A declared secret is different: when a pull can't find it, the entry an earlier pull wrote stays as it is, so it may hold a value that was since rotated, until a pull finds the new one (see [Team secrets](designs/team-secrets.md#a-missing-secret-keeps-the-mcp-entry)). An interactive `pull`, `teamai mcp list`, `teamai env list`, `teamai doctor` and `teamai env exec` name a declared secret with no value, the servers that use it and the command that sets it: `` github: GITHUB_TOKEN is not set. Run `teamai env set GITHUB_TOKEN` (). `` diff --git a/docs/usage-guide.zh-CN.md b/docs/usage-guide.zh-CN.md index d44a34643..c6f4aa2d8 100644 --- a/docs/usage-guide.zh-CN.md +++ b/docs/usage-guide.zh-CN.md @@ -1201,7 +1201,7 @@ TeamAI 仅在所有权记录证明已完成顶层写入且内容仍匹配时, HTTP local-agent 更新 JSON MCP 配置时,先保留原有 ownership 记录,配置写入成功后才更新记录;如果随后保存记录失败,会恢复原配置。`uninstall_mcp` 先删除配置中的条目,再移除 ownership 记录:配置写入失败或无法读取时保留记录以便重试,manifest 写入失败时恢复条目。MCP reconcile 在保存 ownership 前失败时,会恢复本次已写入的所有配置,包括多个工具共用的文件。恢复本身也失败时,错误会同时说明两次失败及受影响的文件;修复配置与 ownership 记录后再重试。只要凭据仍在文件中,就继续保留 Git 排除保护。 -Copilot 使用原生 `mcpServers` 结构:`stdio` 写成 `type: "local"`,远程传输保留 `http` 或 `sse`,每个 TeamAI 管理的条目都会带上必需的 `tools: ["*"]` 允许列表。TeamAI 遵循 `COPILOT_HOME`,项目配置使用 Copilot CLI 官方文档指定的 `.github/mcp.json` 仓库路径。详见 [GitHub Copilot CLI 添加 MCP Server](https://docs.github.com/zh/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers)。Codex 支持 `stdio` 与 `http`,`sse` 会被跳过。Qoder 使用对应作用域 `.qoder/settings.json` 中与 Claude 兼容的 `mcpServers` 格式。Kiro 在专用的、只含 `mcpServers` 的 `.kiro/settings/mcp.json` 中使用同一格式(见 [Kiro MCP 配置文档](https://kiro.dev/docs/mcp/configuration/))。OpenCode 支持 `stdio`(写成其 `type:"local"` 形态)与 `http`(`type:"remote"`),`sse` 会被跳过,其 server 位于共享 `opencode.json` 的 `mcp` 键下。归属记录在 `~/.teamai/managed-mcp.json`——手动添加的 server 不动;与手写同名则跳过,除非 `--force`。 +Copilot 使用原生 `mcpServers` 结构:`stdio` 写成 `type: "local"`,远程传输保留 `http` 或 `sse`,每个 TeamAI 管理的条目都会带上必需的 `tools: ["*"]` 允许列表。TeamAI 遵循 `COPILOT_HOME`,项目配置使用 Copilot CLI 官方文档指定的 `.github/mcp.json` 仓库路径。详见 [GitHub Copilot CLI 添加 MCP Server](https://docs.github.com/zh/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers)。Codex 支持 `stdio` 与 `http`,`sse` 会被跳过。Qoder 使用对应作用域 `.qoder/settings.json` 中与 Claude 兼容的 `mcpServers` 格式。Kiro 在专用的、只含 `mcpServers` 的 `.kiro/settings/mcp.json` 中使用同一格式(见 [Kiro MCP 配置文档](https://kiro.dev/docs/mcp/configuration/))。OpenCode 支持 `stdio`(写成其 `type:"local"` 形态)、`http` 和 `sse`(后两者均为 `type:"remote"`,由客户端协商传输协议),其 server 位于共享 `opencode.json` 的 `mcp` 键下。归属记录在 `~/.teamai/managed-mcp.json`——手动添加的 server 不动;与手写同名则跳过,除非 `--force`。 **密钥**:在 `mcp.yaml` 里写 `${VAR}`,不要写明文。团队在 `env/secrets.yaml` 中声明的 key 优先取你为该团队设置的值(`teamai env set`),其次取你为本机设置的值(`teamai env set --global`),再次取你自己的环境,不包括 teamai `env.sh` 导出的值(见[团队密钥](designs/team-secrets.zh-CN.md#解析顺序))。其他变量优先取你为该团队设置的值(`teamai env set KEY`),其次是该目录收到的团队环境变量(`env/env.yaml` 与活动的 `env//env.yaml`);环境只补充团队没有设置的 key,不再覆盖团队变量(见[团队密钥](designs/team-secrets.zh-CN.md#变量))。你导出的值与团队的值不同而被忽略时,交互式 `pull` 和 `teamai doctor` 会指出。变量无法解析则跳过并提示。已声明的密钥不同:pull 找不到它时,之前某次 pull 写入的条目原样保留,因此里面可能是已经轮换掉的旧值,直到某次 pull 找到新值(见[团队密钥](designs/team-secrets.zh-CN.md#缺少密钥时保留-mcp-条目))。交互式 `pull`、`teamai mcp list`、`teamai env list`、`teamai doctor` 和 `teamai env exec` 会指出没有值的已声明密钥、用到它的 server 以及设置它的命令:`` github: GITHUB_TOKEN is not set. Run `teamai env set GITHUB_TOKEN` (). `` diff --git a/src/__tests__/mcp-reconcile.test.ts b/src/__tests__/mcp-reconcile.test.ts index ef75c6271..32f6048ba 100644 --- a/src/__tests__/mcp-reconcile.test.ts +++ b/src/__tests__/mcp-reconcile.test.ts @@ -4097,16 +4097,17 @@ servers: }); }); - it('renders a remote (http) server with url + headers', async () => { + it.each(['http', 'sse'])('renders a remote (%s) server with url + headers', async (transport) => { await writeMcpYaml(` servers: - name: remote-srv - transport: http + transport: ${transport} url: https://example.com/mcp headers: Authorization: Bearer tok `); - await reconcileMcpForConfig(teamConfig, localConfig); + const result = await reconcileMcpForConfig(teamConfig, localConfig); + expect(result.changes).toContainEqual({ tool: 'opencode', server: 'remote-srv', action: 'added' }); const doc = await fse.readJson(ocConfig()); expect(doc.mcp['remote-srv']).toEqual({ @@ -4115,6 +4116,7 @@ servers: headers: { Authorization: 'Bearer tok' }, enabled: true, }); + expect((await reconcileMcpForConfig(teamConfig, localConfig)).wrote).toBe(false); }); it('preserves unrelated keys (instructions) and the user\'s own mcp entries', async () => { diff --git a/src/resources/mcp-format.ts b/src/resources/mcp-format.ts index b235ae98a..a3976a661 100644 --- a/src/resources/mcp-format.ts +++ b/src/resources/mcp-format.ts @@ -66,8 +66,8 @@ const SUPPORTED_TRANSPORTS: Record> = { // no SSE transport, so only that one is skipped. codex: new Set(['stdio', 'http']), // OpenCode splits transports into `type: local` (stdio) and `type: remote` - // (streamable HTTP). It has no SSE transport. - opencode: new Set(['stdio', 'http']), + // (streamable HTTP or SSE, negotiated by its client). + opencode: new Set(['stdio', 'http', 'sse']), copilot: new Set(['stdio', 'http', 'sse']), }; @@ -271,7 +271,7 @@ function renderCopilot(def: McpServerDef): McpJsonEntry { /** * OpenCode's shape is unlike the others: transport is expressed as - * `type: "local"` (stdio) or `type: "remote"` (http), a local server's command + * `type: "local"` (stdio) or `type: "remote"` (http/sse), a local server's command * and args are a single `command` array, env is `environment` (not `env`), and * every server carries `enabled: true`. */