Skip to content

Commit 4ece4cd

Browse files
Standardize MCP server config across AI agents (#61)
* feat: add mcpServers support plan * feat: add mcpServers support implementation * fix: empty mcpServers
1 parent dc83711 commit 4ece4cd

32 files changed

+2040
-93
lines changed
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
---
2+
phase: design
3+
title: "MCP Config Standardization — Design"
4+
description: "Architecture and data models for universal MCP server configuration"
5+
---
6+
7+
# Design: MCP Config Standardization
8+
9+
## Architecture Overview
10+
11+
```mermaid
12+
graph TD
13+
subgraph Input
14+
T[YAML/JSON Template] -->|ai-devkit init -t| IT[InitTemplate]
15+
C[.ai-devkit.json] -->|ai-devkit install| CM[ConfigManager]
16+
end
17+
18+
subgraph Core
19+
IT -->|persist mcpServers| CM
20+
CM -->|read mcpServers| IS[InstallService]
21+
IS --> MG[McpConfigGenerator]
22+
end
23+
24+
subgraph Generators
25+
MG --> CG[ClaudeCodeGenerator]
26+
MG --> XG[CodexGenerator]
27+
end
28+
29+
subgraph Output
30+
CG -->|write/merge| MJ[.mcp.json]
31+
XG -->|write/merge| CT[.codex/config.toml]
32+
end
33+
34+
subgraph Safety
35+
CG -->|detect existing| EP[ExistingConfigParser]
36+
XG -->|detect existing| EP
37+
EP -->|conflicts?| PR[Prompt User]
38+
end
39+
```
40+
41+
**Flow:**
42+
1. User defines `mcpServers` in `.ai-devkit.json` directly or via a YAML template (`ai-devkit init -t`)
43+
2. `ai-devkit install` reads `mcpServers` from config
44+
3. `McpConfigGenerator` dispatches to per-agent generators based on `config.environments`
45+
4. Each generator reads the existing agent config (if any), computes a diff, and prompts the user before writing
46+
47+
## Data Models
48+
49+
### Universal MCP Server Schema (in `.ai-devkit.json`)
50+
51+
```typescript
52+
// Added to DevKitConfig
53+
interface DevKitConfig {
54+
// ... existing fields ...
55+
mcpServers?: Record<string, McpServerDefinition>;
56+
}
57+
58+
// Universal MCP server definition
59+
interface McpServerDefinition {
60+
// Transport type
61+
transport: 'stdio' | 'http' | 'sse';
62+
63+
// stdio transport fields
64+
command?: string; // Required for stdio
65+
args?: string[]; // Optional for stdio
66+
env?: Record<string, string>; // Optional environment variables
67+
68+
// http/sse transport fields
69+
url?: string; // Required for http/sse
70+
headers?: Record<string, string>; // Optional HTTP headers (auth, etc.)
71+
}
72+
```
73+
74+
### Template Schema Extension
75+
76+
```typescript
77+
// Added to InitTemplateConfig
78+
interface InitTemplateConfig {
79+
// ... existing fields ...
80+
mcpServers?: Record<string, McpServerDefinition>;
81+
}
82+
```
83+
84+
### Example `.ai-devkit.json`
85+
86+
```json
87+
{
88+
"version": "0.5.0",
89+
"environments": ["claude", "codex"],
90+
"phases": ["requirements", "design", "planning", "implementation", "testing"],
91+
"mcpServers": {
92+
"memory": {
93+
"transport": "stdio",
94+
"command": "npx",
95+
"args": ["-y", "@ai-devkit/memory"],
96+
"env": { "MEMORY_DB_PATH": "./memory.db" }
97+
},
98+
"filesystem": {
99+
"transport": "stdio",
100+
"command": "npx",
101+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./src"]
102+
},
103+
"notion": {
104+
"transport": "http",
105+
"url": "https://mcp.notion.com/mcp"
106+
},
107+
"secure-api": {
108+
"transport": "http",
109+
"url": "https://api.example.com/mcp",
110+
"headers": {
111+
"Authorization": "Bearer ${API_KEY}"
112+
}
113+
}
114+
}
115+
}
116+
```
117+
118+
### Example YAML Template
119+
120+
```yaml
121+
environments:
122+
- claude
123+
- codex
124+
phases:
125+
- requirements
126+
- design
127+
- planning
128+
- implementation
129+
- testing
130+
skills:
131+
- registry: codeaholicguy/ai-devkit
132+
skill: memory
133+
mcpServers:
134+
memory:
135+
transport: stdio
136+
command: npx
137+
args: ["-y", "@ai-devkit/memory"]
138+
env:
139+
MEMORY_DB_PATH: "./memory.db"
140+
notion:
141+
transport: http
142+
url: https://mcp.notion.com/mcp
143+
```
144+
145+
## Agent-Specific Output Formats
146+
147+
### Claude Code (`.mcp.json`)
148+
149+
stdio servers omit the `type` field (inferred). HTTP/SSE servers use `"type": "http"` or `"type": "sse"`.
150+
151+
```json
152+
{
153+
"mcpServers": {
154+
"memory": {
155+
"command": "npx",
156+
"args": ["-y", "@ai-devkit/memory"],
157+
"env": { "MEMORY_DB_PATH": "./memory.db" }
158+
},
159+
"notion": {
160+
"type": "http",
161+
"url": "https://mcp.notion.com/mcp"
162+
},
163+
"secure-api": {
164+
"type": "http",
165+
"url": "https://api.example.com/mcp",
166+
"headers": {
167+
"Authorization": "Bearer ${API_KEY}"
168+
}
169+
}
170+
}
171+
}
172+
```
173+
174+
**Mapping rules (universal → Claude Code):**
175+
- `transport: "stdio"` → omit `type`, emit `command`/`args`/`env`
176+
- `transport: "http"``"type": "http"`, emit `url`/`headers`
177+
- `transport: "sse"``"type": "sse"`, emit `url`/`headers`
178+
- `headers` → `headers` (direct pass-through)
179+
180+
### Codex (`.codex/config.toml`)
181+
182+
```toml
183+
[mcp_servers.memory]
184+
command = "npx"
185+
args = ["-y", "@ai-devkit/memory"]
186+
187+
[mcp_servers.memory.env]
188+
MEMORY_DB_PATH = "./memory.db"
189+
190+
[mcp_servers.notion]
191+
url = "https://mcp.notion.com/mcp"
192+
193+
[mcp_servers.secure-api]
194+
url = "https://api.example.com/mcp"
195+
196+
[mcp_servers.secure-api.http_headers]
197+
Authorization = "Bearer ${API_KEY}"
198+
```
199+
200+
**Mapping rules (universal → Codex):**
201+
- `transport: "stdio"` → emit `command`/`args`; `env` as `[mcp_servers.<name>.env]` table
202+
- `transport: "http"` or `"sse"` → emit `url`; `headers` as `[mcp_servers.<name>.http_headers]` table
203+
- Codex-specific fields (`startup_timeout_sec`, `enabled_tools`, etc.) are not generated in v1
204+
205+
## Component Breakdown
206+
207+
### 1. Schema & Validation (`types.ts` + `InitTemplate.ts`)
208+
- `McpTransport` type and `McpServerDefinition` interface in `types.ts`
209+
- `mcpServers` added to `DevKitConfig` (optional)
210+
- `mcpServers` added to `InitTemplateConfig` and `ALLOWED_TEMPLATE_FIELDS`
211+
- Validation extracted to `validateMcpServers()` and `validateStringRecord()` helpers
212+
- Validate: transport required (`stdio`|`http`|`sse`), stdio requires `command`, http/sse requires `url`, `headers` optional for http/sse
213+
214+
### 2. ConfigManager (`lib/Config.ts`)
215+
- No code changes needed — existing generic `update()`/`read()` handles `mcpServers` via the updated `DevKitConfig` type
216+
217+
### 3. MCP Generators (`services/install/mcp/`)
218+
219+
```
220+
services/install/mcp/
221+
├── index.ts # Re-exports: installMcpServers, McpInstallOptions, McpInstallReport
222+
├── types.ts # McpAgentGenerator interface, McpMergePlan, McpInstallReport
223+
├── BaseMcpGenerator.ts # Abstract base: shared plan() + apply() diff-and-merge logic
224+
├── McpConfigGenerator.ts # Orchestrator: dispatch to generators, conflict resolution, CI mode
225+
├── ClaudeCodeMcpGenerator.ts # .mcp.json: toAgentFormat + read/write JSON
226+
└── CodexMcpGenerator.ts # .codex/config.toml: toAgentFormat + read/write TOML
227+
```
228+
229+
**Key interfaces:**
230+
```typescript
231+
interface McpAgentGenerator {
232+
readonly agentType: EnvironmentCode;
233+
plan(servers: Record<string, McpServerDefinition>, projectRoot: string): Promise<McpMergePlan>;
234+
apply(plan: McpMergePlan, servers: Record<string, McpServerDefinition>, projectRoot: string): Promise<void>;
235+
}
236+
237+
interface McpMergePlan {
238+
agentType: EnvironmentCode;
239+
newServers: string[];
240+
conflictServers: string[];
241+
skippedServers: string[];
242+
resolvedConflicts: string[]; // filled after user prompt or --overwrite
243+
}
244+
```
245+
246+
**`BaseMcpGenerator`** provides shared `plan()` and `apply()` logic. Subclasses implement three abstract methods:
247+
- `toAgentFormat(def)` — convert universal schema to agent-native format
248+
- `readExistingServers(projectRoot)` — read and parse agent config file
249+
- `writeServers(projectRoot, mergedServers)` — serialize and write back
250+
251+
### 4. Merge & Conflict Resolution
252+
- **Comparison**: `deepEqual()` (shared util at `util/object.ts`) on agent-format output
253+
- **Interactive mode**: prompt user with skip all / overwrite all / choose per server
254+
- **Non-interactive (CI)**: `--overwrite` flag → overwrite all; default → skip all conflicts
255+
- **TTY detection**: `isInteractiveTerminal()` (shared util at `util/terminal.ts`)
256+
- **Preservation**: both generators read the full config, modify only MCP server entries, and preserve all other content
257+
258+
### 5. Install Service Extension (`services/install/install.service.ts`)
259+
- Calls `installMcpServers()` after skills install, passing `{ overwrite }` from CLI options
260+
- Uses union of existing + newly installed environments to determine which generators to run
261+
- `McpInstallReport` added to `InstallReport` (installed/skipped/conflicts/failed counts)
262+
- Persists `mcpServers` to `.ai-devkit.json` via `configManager.update()`
263+
264+
### 6. Install Config Validation (`util/config.ts`)
265+
- `mcpServers` added to `InstallConfigData` with Zod schema validation
266+
- Defaults to `{}` when absent — existing configs work unchanged
267+
268+
### 7. Init Command (`commands/init.ts`)
269+
- Persists `mcpServers` from template to `.ai-devkit.json` after skills install
270+
- Displays count and suggests running `ai-devkit install` to generate agent configs
271+
272+
## Design Decisions
273+
274+
| Decision | Choice | Rationale |
275+
|----------|--------|-----------|
276+
| Config location | `mcpServers` in `.ai-devkit.json` | Single source of truth, no new files |
277+
| Transport field | Explicit `transport: 'stdio' \| 'http' \| 'sse'` | Clear intent, easy validation; maps to agent-specific format per generator |
278+
| `http` naming | Use `http` not `streamable-http` | Matches Claude Code's `type: "http"` convention; shorter; the MCP spec calls it "Streamable HTTP" but agents use `http` |
279+
| SSE support | Include but mark deprecated | SSE is deprecated in MCP spec (replaced by streamable-http) but some servers still use it |
280+
| Merge strategy | Additive with prompt on conflict | Prevents data loss, respects user customizations |
281+
| CI / non-interactive | Skip conflicts by default, overwrite with `--overwrite` | Safe default for CI pipelines; no hanging prompts |
282+
| TOML library | `smol-toml` | Codex config needs both read and write; `smol-toml` handles nested tables correctly |
283+
| Generator pattern | Abstract base class + subclasses | Shared plan/apply logic in `BaseMcpGenerator`; subclasses only provide format-specific I/O |
284+
| Scope | Project-level only | `.mcp.json` for Claude Code, `.codex/config.toml` for Codex — no user-level configs |
285+
286+
## Non-Functional Requirements
287+
288+
- **Performance**: Config generation is fast (file I/O only, no network)
289+
- **Safety**: Never silently overwrite existing configs; prompt in interactive mode, skip in CI
290+
- **Extensibility**: Adding a new agent generator requires only a new subclass of `BaseMcpGenerator`
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
phase: implementation
3+
title: "MCP Config Standardization — Implementation"
4+
description: "Technical implementation notes for MCP config generation"
5+
---
6+
7+
# Implementation: MCP Config Standardization
8+
9+
## Development Setup
10+
11+
- Worktree: `.worktrees/feature-mcp-config`
12+
- Branch: `feature-mcp-config`
13+
- Dependencies: `npm ci` in worktree root
14+
15+
## Code Structure
16+
17+
```
18+
packages/cli/src/
19+
├── types.ts # McpTransport, McpServerDefinition, DevKitConfig.mcpServers
20+
├── lib/
21+
│ ├── Config.ts # No changes — generic update/read handles mcpServers
22+
│ ├── InitTemplate.ts # mcpServers validation (validateMcpServers, validateStringRecord)
23+
│ └── SkillManager.ts # Refactored to use shared isInteractiveTerminal()
24+
├── commands/
25+
│ ├── init.ts # Persist mcpServers from template to config
26+
│ └── install.ts # Report MCP results in summary
27+
├── services/
28+
│ └── install/
29+
│ ├── install.service.ts # Call installMcpServers() with --overwrite passthrough
30+
│ └── mcp/
31+
│ ├── index.ts # Re-exports: installMcpServers, McpInstallOptions, McpInstallReport
32+
│ ├── types.ts # McpAgentGenerator, McpMergePlan, McpInstallReport
33+
│ ├── BaseMcpGenerator.ts # Abstract base: shared plan() + apply() logic
34+
│ ├── McpConfigGenerator.ts # Orchestrator: dispatch, conflict resolution, CI mode
35+
│ ├── ClaudeCodeMcpGenerator.ts # .mcp.json generator
36+
│ └── CodexMcpGenerator.ts # .codex/config.toml generator
37+
└── util/
38+
├── config.ts # mcpServers in InstallConfigData + Zod schema
39+
├── object.ts # deepEqual() — shared recursive comparison
40+
└── terminal.ts # isInteractiveTerminal() — shared TTY detection
41+
```
42+
43+
## Implementation Notes
44+
45+
### Core Features
46+
47+
**McpServerDefinition validation:** Manual validation in `InitTemplate.ts` matching existing patterns. Extracted to `validateMcpServers()` and `validateStringRecord()` helpers. Validates transport (`stdio`|`http`|`sse`), `command` required for stdio, `url` required for http/sse. Also validated via Zod in `util/config.ts` for the install path.
48+
49+
**Generator architecture:**
50+
- `BaseMcpGenerator` — abstract base with shared `plan()` and `apply()` diff-and-merge logic
51+
- Subclasses implement 3 abstract methods: `toAgentFormat()`, `readExistingServers()`, `writeServers()`
52+
- `McpConfigGenerator` — orchestrator that dispatches to generators and handles conflict resolution
53+
54+
**Conflict resolution (interactive vs CI):**
55+
- Interactive (TTY): `inquirer` prompt — skip all / overwrite all / choose per server
56+
- Non-interactive: `--overwrite` → overwrite all; default → skip all (no hanging prompts)
57+
- Detection via shared `isInteractiveTerminal()` in `util/terminal.ts`
58+
59+
### Patterns & Best Practices
60+
61+
- Abstract base class eliminates duplicated plan/apply logic between generators
62+
- Follows existing `install.service.ts` report structure (`installed`/`skipped`/`failed` counts)
63+
- Follows existing `InitTemplate.ts` validation patterns (manual validation, clear field-path error messages)
64+
- `inquirer` for interactive prompts (already a dependency)
65+
- `fullConfig` instance field in each generator preserves non-MCP content between read → write
66+
67+
## Integration Points
68+
69+
- `ConfigManager.read()` → returns `mcpServers` from `.ai-devkit.json` (no code changes needed)
70+
- `InitTemplate.loadInitTemplate()` → validates and returns `mcpServers` from template
71+
- `reconcileAndInstall()` → calls `installMcpServers()` after skills section, passes `{ overwrite }` from CLI
72+
- `installCommand()` → reports MCP results in install summary
73+
74+
## Error Handling
75+
76+
- Invalid `mcpServers` in template → validation error with field path (e.g., `"mcpServers.memory.command" is required for stdio transport`)
77+
- Existing config file parse failure → treat as empty (catch block), don't block install
78+
- Generator failure → report as failed in `McpInstallReport`, continue with other agents
79+
- Overall MCP failure → push to `report.warnings`, don't affect exit code for environment/phase failures

0 commit comments

Comments
 (0)