cd84ccfb75
fixes stale docs/schema that referenced ~/.omp/mcp.json Related: #462, #264
227 lines
9.0 KiB
Markdown
227 lines
9.0 KiB
Markdown
# MCP server and tool authoring
|
|
|
|
This document explains how MCP server definitions become callable `mcp_*` tools in coding-agent, and what operators should expect when configs are invalid, duplicated, disabled, or auth-gated.
|
|
|
|
## Architecture at a glance
|
|
|
|
```text
|
|
Config sources (.omp/.claude/.cursor/.vscode/mcp.json, mcp.json, etc.)
|
|
-> discovery providers normalize to canonical MCPServer
|
|
-> capability loader dedupes by server name (higher provider priority wins)
|
|
-> loadAllMCPConfigs converts to MCPServerConfig + skips enabled:false
|
|
-> MCPManager connects/listTools (with auth/header/env resolution)
|
|
-> MCPTool/DeferredMCPTool bridge exposes tools as mcp_<server>_<tool>
|
|
-> AgentSession.refreshMCPTools replaces live MCP tools immediately
|
|
```
|
|
|
|
## 1) Server config model and validation
|
|
|
|
`src/mcp/types.ts` defines the authoring shape used by MCP config writers and runtime:
|
|
|
|
- `stdio` (default when `type` missing): requires `command`, optional `args`, `env`, `cwd`
|
|
- `http`: requires `url`, optional `headers`
|
|
- `sse`: requires `url`, optional `headers` (kept for compatibility)
|
|
- shared fields: `enabled`, `timeout`, `auth`
|
|
|
|
`validateServerConfig()` (`src/mcp/config.ts`) enforces transport basics:
|
|
|
|
- rejects configs that set both `command` and `url`
|
|
- requires `command` for stdio
|
|
- requires `url` for http/sse
|
|
- rejects unknown `type`
|
|
|
|
`config-writer.ts` applies this validation for add/update operations and also validates server names:
|
|
|
|
- non-empty
|
|
- max 100 chars
|
|
- only `[a-zA-Z0-9_.-]`
|
|
|
|
### Transport pitfalls
|
|
|
|
- `type` omitted means stdio. If you intended HTTP/SSE but omitted `type`, `command` becomes mandatory.
|
|
- `sse` is still accepted but treated as HTTP transport internally (`createHttpTransport`).
|
|
- Validation is structural, not reachability: a syntactically valid URL can still fail at connect time.
|
|
|
|
## 2) Discovery, normalization, and precedence
|
|
|
|
### Capability-based discovery
|
|
|
|
`loadAllMCPConfigs()` (`src/mcp/config.ts`) loads canonical `MCPServer` items via `loadCapability(mcpCapability.id)`.
|
|
|
|
The capability layer (`src/capability/index.ts`) then:
|
|
|
|
1. loads providers in priority order
|
|
2. dedupes by `server.name` (first win = highest priority)
|
|
3. validates deduped items
|
|
|
|
Result: duplicate server names across sources are not merged. One definition wins; lower-priority duplicates are shadowed.
|
|
|
|
### `.mcp.json` and related files
|
|
|
|
The dedicated fallback provider in `src/discovery/mcp-json.ts` reads project-root `mcp.json` and `.mcp.json` (low priority).
|
|
|
|
In practice MCP servers also come from higher-priority providers (for example native `.omp/...` and tool-specific config dirs). Authoring guidance:
|
|
|
|
- Prefer `.omp/mcp.json` (project) or `~/.omp/agent/mcp.json` (user) for explicit control.
|
|
- Use root `mcp.json` / `.mcp.json` when you need fallback compatibility.
|
|
- Reusing the same server name in multiple sources causes precedence shadowing, not merge.
|
|
|
|
### Normalization behavior
|
|
|
|
`convertToLegacyConfig()` (`src/mcp/config.ts`) maps canonical `MCPServer` to runtime `MCPServerConfig`.
|
|
|
|
Key behavior:
|
|
|
|
- transport inferred as `server.transport ?? (command ? "stdio" : url ? "http" : "stdio")`
|
|
- disabled servers (`enabled === false`) are dropped before connection
|
|
- optional fields are preserved when present
|
|
|
|
### Environment expansion during discovery
|
|
|
|
`mcp-json.ts` expands env placeholders in string fields with `expandEnvVarsDeep()`:
|
|
|
|
- supports `${VAR}` and `${VAR:-default}`
|
|
- unresolved values remain literal `${VAR}` strings
|
|
|
|
`mcp-json.ts` also performs runtime type checks for user JSON and logs warnings for invalid `enabled`/`timeout` values instead of hard-failing the whole file.
|
|
|
|
## 3) Auth and runtime value resolution
|
|
|
|
`MCPManager.prepareConfig()`/`#resolveAuthConfig()` (`src/mcp/manager.ts`) is the final pre-connect pass.
|
|
|
|
### OAuth credential injection
|
|
|
|
If config has:
|
|
|
|
```ts
|
|
auth: { type: "oauth", credentialId: "..." }
|
|
```
|
|
|
|
and credential exists in auth storage:
|
|
|
|
- `http`/`sse`: injects `Authorization: Bearer <access_token>` header
|
|
- `stdio`: injects `OAUTH_ACCESS_TOKEN` env var
|
|
|
|
If credential lookup fails, manager logs a warning and continues with unresolved auth.
|
|
|
|
### Header/env value resolution
|
|
|
|
Before connect, manager resolves each header/env value via `resolveConfigValue()` (`src/config/resolve-config-value.ts`):
|
|
|
|
- value starting with `!` => execute shell command, use trimmed stdout (cached)
|
|
- otherwise, treat value as environment variable name first (`process.env[name]`), fallback to literal value
|
|
- unresolved command/env values are omitted from final headers/env map
|
|
|
|
Operational caveat: this means a mistyped secret command/env key can silently remove that header/env entry, producing downstream 401/403 or server startup failures.
|
|
|
|
## 4) Tool bridge: MCP -> agent-callable tools
|
|
|
|
`src/mcp/tool-bridge.ts` converts MCP tool definitions into `CustomTool`s.
|
|
|
|
### Naming and collision domain
|
|
|
|
Tool names are generated as:
|
|
|
|
```text
|
|
mcp_<sanitized_server_name>_<sanitized_tool_name>
|
|
```
|
|
|
|
Rules:
|
|
|
|
- lowercases
|
|
- non-`[a-z_]` chars become `_`
|
|
- repeated underscores collapse
|
|
- redundant `<server>_` prefix in tool name is stripped once
|
|
|
|
This avoids many collisions, but not all. Different raw names can still sanitize to the same identifier (for example `my-server` and `my.server` both sanitize similarly), and registry insertion is last-write-wins.
|
|
|
|
### Schema mapping
|
|
|
|
`convertSchema()` keeps MCP JSON Schema mostly as-is but patches object schemas missing `properties` with `{}` for provider compatibility.
|
|
|
|
### Execution mapping
|
|
|
|
`MCPTool.execute()` / `DeferredMCPTool.execute()`:
|
|
|
|
- calls MCP `tools/call`
|
|
- flattens MCP content into displayable text
|
|
- returns structured details (`serverName`, `mcpToolName`, provider metadata)
|
|
- maps server-reported `isError` to `Error: ...` text result
|
|
- maps thrown transport/runtime failures to `MCP error: ...`
|
|
- preserves abort semantics by translating AbortError into `ToolAbortError`
|
|
|
|
## 5) Operator lifecycle: add/edit/remove and live updates
|
|
|
|
Interactive mode exposes `/mcp` in `src/modes/controllers/mcp-command-controller.ts`.
|
|
|
|
Supported operations:
|
|
|
|
- `add` (wizard or quick-add)
|
|
- `remove` / `rm`
|
|
- `enable` / `disable`
|
|
- `test`
|
|
- `reauth` / `unauth`
|
|
- `reload`
|
|
|
|
Config writes are atomic (`writeMCPConfigFile`: temp file + rename).
|
|
|
|
After changes, controller calls `#reloadMCP()`:
|
|
|
|
1. `mcpManager.disconnectAll()`
|
|
2. `mcpManager.discoverAndConnect()`
|
|
3. `session.refreshMCPTools(mcpManager.getTools())`
|
|
|
|
`refreshMCPTools()` replaces all `mcp_` registry entries and immediately re-activates the latest MCP tool set, so changes take effect without restarting the session.
|
|
|
|
### Mode differences
|
|
|
|
- **Interactive/TUI mode**: `/mcp` gives in-app UX (wizard, OAuth flow, connection status text, immediate runtime rebinding).
|
|
- **SDK/headless integration**: `discoverAndLoadMCPTools()` (`src/mcp/loader.ts`) returns loaded tools + per-server errors; no `/mcp` command UX.
|
|
|
|
## 6) User-visible error surfaces
|
|
|
|
Common error strings users/operators see:
|
|
|
|
- add/update validation failures:
|
|
- `Invalid server config: ...`
|
|
- `Server "<name>" already exists in <path>`
|
|
- quick-add argument issues:
|
|
- `Use either --url or -- <command...>, not both.`
|
|
- `--token requires --url (HTTP/SSE transport).`
|
|
- connect/test failures:
|
|
- `Failed to connect to "<name>": <message>`
|
|
- timeout help text suggests increasing timeout
|
|
- auth help text for `401/403`
|
|
- auth/OAuth flows:
|
|
- `Authentication required ... OAuth endpoints could not be discovered`
|
|
- `OAuth flow timed out. Please try again.`
|
|
- `OAuth authentication failed: ...`
|
|
- disabled server usage:
|
|
- `Server "<name>" is disabled. Run /mcp enable <name> first.`
|
|
|
|
Bad source JSON in discovery is generally handled as warnings/logs; config-writer paths throw explicit errors.
|
|
|
|
## 7) Practical authoring guidance
|
|
|
|
For robust MCP authoring in this codebase:
|
|
|
|
1. Keep server names globally unique across all MCP-capable config sources.
|
|
2. Prefer alphanumeric/underscore names to avoid sanitized-name collisions in generated `mcp_*` tool names.
|
|
3. Use explicit `type` to avoid accidental stdio defaults.
|
|
4. Treat `enabled: false` as hard-off: server is omitted from runtime connect set.
|
|
5. For OAuth configs, store a valid `credentialId`; otherwise auth injection is skipped.
|
|
6. If using command-based secret resolution (`!cmd`), verify command output is stable and non-empty.
|
|
|
|
## Implementation files
|
|
|
|
- [`src/mcp/types.ts`](../packages/coding-agent/src/mcp/types.ts)
|
|
- [`src/mcp/config.ts`](../packages/coding-agent/src/mcp/config.ts)
|
|
- [`src/mcp/config-writer.ts`](../packages/coding-agent/src/mcp/config-writer.ts)
|
|
- [`src/mcp/tool-bridge.ts`](../packages/coding-agent/src/mcp/tool-bridge.ts)
|
|
- [`src/discovery/mcp-json.ts`](../packages/coding-agent/src/discovery/mcp-json.ts)
|
|
- [`src/modes/controllers/mcp-command-controller.ts`](../packages/coding-agent/src/modes/controllers/mcp-command-controller.ts)
|
|
- [`src/mcp/manager.ts`](../packages/coding-agent/src/mcp/manager.ts)
|
|
- [`src/capability/index.ts`](../packages/coding-agent/src/capability/index.ts)
|
|
- [`src/config/resolve-config-value.ts`](../packages/coding-agent/src/config/resolve-config-value.ts)
|
|
- [`src/mcp/loader.ts`](../packages/coding-agent/src/mcp/loader.ts)
|