Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion apps/cli/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// CLI slash commands — pure functions over a session context.
// Spec: docs/DEVELOPMENT_PLAN.md §3.6 (30+ commands; M2 ships a core subset)

import type { DeepCodeSettings, SessionManager, SessionMeta } from '@deepcode/core';
import type {
DeepCodeSettings,
McpClientHandle,
SessionManager,
SessionMeta,
} from '@deepcode/core';
import { redact, type Credentials } from '@deepcode/core';

export interface SessionContext {
Expand All @@ -18,6 +23,10 @@ export interface SessionContext {
exitRequested?: boolean;
/** Replace history entirely (used by /clear, /resume). */
clearHistory?: boolean;
/** Connected MCP server handles (M3c). */
mcpServers?: McpClientHandle[];
/** MCP servers that failed to connect on startup (M3c). */
mcpErrors?: Array<{ serverName: string; error: string }>;
}

export interface SlashCommand {
Expand Down Expand Up @@ -215,6 +224,37 @@ export const InitCommand: SlashCommand = {
},
};

export const McpCommand: SlashCommand = {
name: '/mcp',
description: 'List connected MCP servers and their tools.',
async run(_args, ctx) {
const servers = ctx.mcpServers ?? [];
if (servers.length === 0) {
return [
'No MCP servers connected.',
'',
'Add servers in settings.json under "mcpServers". Example:',
' { "mcpServers": { "filesystem": { "command": "npx",',
' "args": ["@modelcontextprotocol/server-filesystem", "/tmp"] } } }',
];
}
const lines = [`Connected MCP servers (${servers.length}):`];
for (const s of servers) {
lines.push(` ● ${s.serverName} · ${s.tools.length} tools`);
for (const t of s.tools.slice(0, 6)) {
lines.push(` - ${t.name}`);
}
if (s.tools.length > 6) lines.push(` … and ${s.tools.length - 6} more`);
}
if ((ctx.mcpErrors ?? []).length > 0) {
lines.push('');
lines.push('Servers that failed to connect:');
for (const e of ctx.mcpErrors!) lines.push(` ✕ ${e.serverName} ${e.error}`);
}
return lines;
},
};

export const TodosCommand: SlashCommand = {
name: '/todos',
description: 'Show active TODO list (M3 wires TodoWrite tool).',
Expand All @@ -237,6 +277,7 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [
AddDirCommand,
ResumeCommand,
InitCommand,
McpCommand,
TodosCommand,
];

Expand Down
35 changes: 35 additions & 0 deletions apps/cli/src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
ToolRegistry,
applyStyle,
buildSkillsDescriptionBlock,
closeAllMcpServers,
connectAllMcpServers,
findStyle,
loadMemory,
loadOutputStyles,
Expand All @@ -20,6 +22,7 @@ import {
runAgent,
type DeepCodeSettings,
type Effort,
type McpClientHandle,
type Mode,
type AgentEvent,
type StoredMessage,
Expand Down Expand Up @@ -98,6 +101,32 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
tools.register(makeSkillTool(skills));
}

// M3c: connect MCP servers (best-effort; individual failures don't abort)
let mcpServers: McpClientHandle[] = [];
let mcpErrors: Array<{ serverName: string; error: string }> = [];
if (settings.mcpServers && Object.keys(settings.mcpServers).length > 0) {
const enabled = settings.enabledMcpjsonServers;
const disabled = settings.disabledMcpjsonServers ?? [];
const result = await connectAllMcpServers(settings.mcpServers, {
enabledOnly: enabled,
disabled,
});
mcpServers = result.handles;
mcpErrors = result.errors;
// Register every MCP-tool handler into the live registry
for (const handle of mcpServers) {
for (const tool of handle.tools) tools.register(tool);
}
if (mcpServers.length > 0) {
output.write(
` ⊞ MCP: ${mcpServers.length} server(s) connected (${mcpServers.reduce((n, h) => n + h.tools.length, 0)} tools)\n`,
);
}
if (mcpErrors.length > 0) {
output.write(` ⊞ MCP: ${mcpErrors.length} server(s) failed (see /mcp)\n`);
}
}

// Build the composite system prompt
let systemPrompt = DEFAULT_SYSTEM_PROMPT;
if (memory.text) systemPrompt += '\n\n' + memory.text;
Expand All @@ -122,6 +151,8 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
sessionId: session.id,
sessions,
usage: { inputTokens: 0, outputTokens: 0, reasoningTokens: 0 },
mcpServers,
mcpErrors,
};

output.write(`\n ▎ DeepCode · ${ctx.model} · mode: ${ctx.mode} · effort: ${ctx.effort}\n`);
Expand Down Expand Up @@ -202,6 +233,10 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
}

rl.close();
// Clean up MCP server connections
if (mcpServers.length > 0) {
await closeAllMcpServers(mcpServers);
}
return 0;
}

Expand Down
8 changes: 4 additions & 4 deletions docs/m1-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

## End-to-end runs

| Scenario | Result |
|---|---|
| Agent reads a file via Read tool | ✓ 2 turns, 2523 in / 137 out tokens, ended `end_turn`, correct answer |
| Scenario | Result |
| ----------------------------------- | ------------------------------------------------------------------------------------- |
| Agent reads a file via Read tool | ✓ 2 turns, 2523 in / 137 out tokens, ended `end_turn`, correct answer |
| Reasoner solves a math word problem | ✓ 1 turn, 1188 in / 500 out / 427 reasoning, both `thinking` + `text` blocks streamed |
| `/v1/models` + alias mapping | ✓ documented in §3.1 update |
| `/v1/models` + alias mapping | ✓ documented in §3.1 update |

## Changes in this PR

Expand Down
44 changes: 44 additions & 0 deletions docs/milestones/M3c-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# M3c · MCP Client (stdio)

> **Status**: ✅ stdio transport shipped + wired into REPL
> **Branch**: `feat/m3c-mcp-client`

## Shipped

- `packages/core/src/mcp/client.ts` — `connectMcpServer(name, config)` + `connectAllMcpServers(servers)` + `closeAllMcpServers(handles)`.
- Uses official `@modelcontextprotocol/sdk@^1.29.0` for protocol details.
- Tools are exposed with **`mcp__<server>__<tool>` qualified name**, registered into the same `ToolRegistry` that powers the 6 P0 tools. The agent loop sees them identically.
- Tool results unwrap MCP's `content[].text` array into a single string for the `tool_result` block.
- Individual server failures are isolated — one bad config doesn't kill others.
- `/mcp` slash command lists connected servers + tool count + errors.
- CLI REPL now auto-connects MCP servers from `settings.mcpServers` on startup, honoring `enabledMcpjsonServers` / `disabledMcpjsonServers`.
- Graceful shutdown closes all connections.

## Tests (6 new)

`packages/core/src/mcp/client.test.ts` spawns tiny in-disk MCP server scripts that import the SDK via absolute path (so /tmp doesn't need node_modules):

1. lists tools and qualifies names
2. calls a remote tool, returns its text output
3. `connectAllMcpServers` continues on individual failures
4. respects `disabled` list
5. respects `enabledOnly` list
6. rejects server config missing `command`

## NOT in this PR (M3c-ext, next)

- HTTP transport
- SSE transport
- OAuth (2.0 client credentials + dynamic flows)
- `headersHelper` (dynamic auth via shell)
- Elicitation hooks (server-requested user input)
- `deepcode mcp serve` (reverse-expose DeepCode as MCP server)
- `_meta["anthropic/maxResultSizeChars"]` per-tool output caps
- MCP resource references in composer (`@server:proto://path`)
- `mcp__server__prompt` slash commands

## Verified

- `pnpm typecheck` → green
- `pnpm test` → 264 passed / 0 failed / 7 skipped (was 258)
- CLI builds: `node apps/cli/dist/cli.js --help` still works
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"clean": "rm -rf dist *.tsbuildinfo"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"openai": "^6.0.0"
},
"devDependencies": {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ export {
type Frontmatter,
} from './skills/index.js';

// MCP client (M3c — stdio transport; http/sse/OAuth/serve → M3c-ext)
export {
connectMcpServer,
connectAllMcpServers,
closeAllMcpServers,
type McpClientHandle,
type McpToolMeta,
type ConnectAllResult,
} from './mcp/index.js';

// Plugins (M5 — manifest + hash pinning + local install + discovery)
export {
installLocal,
Expand Down
Loading
Loading