Skip to content
Open
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,39 @@ const transport = new StdioServerTransport();
await server.connect(transport);
```

### Code Mode Wrapper (experimental)

You can also run a lightweight “code-mode” wrapper that proxies multiple MCP servers through a single stdio endpoint and only loads tool definitions on demand. Create a config file (for example `code-config.mcp-servers.json`) inside your local checkout of this SDK:

```json
{
"downstreams": [
{
"id": "playwright",
"description": "Browser automation via Playwright",
"command": "node",
"args": ["/Users/you/Desktop/playwright-mcp/cli.js", "--headless", "--browser=chromium"]
}
]
}
```

Then launch the wrapper:

```bash
pnpm code-mode -- --config ./code-config.mcp-servers.json
```

Point your MCP client (Cursor, VS Code, etc.) at the wrapper command instead of individual servers. The wrapper publishes four meta-tools:

1. `list_mcp_servers` — enumerate the configured downstream servers (IDs + descriptions).
2. `list_tool_names` — requires a `serverId` and returns just the tool names/descriptions for that server.
3. `get_tool_implementation` — loads the full schema and a generated TypeScript stub for a specific tool.
4. `call_tool` — proxies the downstream tool call unchanged.

This mirrors the progressive disclosure workflow described in Anthropic’s [Code execution with MCP: Building more efficient agents](https://www.anthropic.com/engineering/code-execution-with-mcp): models explore servers first, then drill into tools only when needed. You can keep
many MCP servers configured without loading all of their schemas into the prompt, and the LLM pays the context cost only for the server/tool it decides to use.

### Testing and Debugging

To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
"test:watch": "vitest",
"start": "npm run server",
"server": "tsx watch --clear-screen=false scripts/cli.ts server",
"client": "tsx scripts/cli.ts client"
"client": "tsx scripts/cli.ts client",
"code-mode": "tsx scripts/code-mode.ts"
},
"dependencies": {
"ajv": "^8.17.1",
Expand Down
119 changes: 119 additions & 0 deletions scripts/code-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { CodeModeWrapper } from '../src/code-mode/index.js';
import type { DownstreamConfig } from '../src/code-mode/downstream.js';
import { StdioServerTransport } from '../src/server/stdio.js';
import type { Implementation } from '../src/types.js';

type CodeModeConfig = {
server?: Implementation;
downstreams: DownstreamConfig[];
};

function parseArgs(argv: string[]): string | undefined {
for (let i = 0; i < argv.length; i += 1) {
const current = argv[i];
if (current === '--config' || current === '-c') {
return argv[i + 1];
}

if (current?.startsWith('--config=')) {
return current.split('=')[1];
}
}

return undefined;
}

function assertDownstreamConfig(value: unknown): asserts value is DownstreamConfig[] {
if (!Array.isArray(value) || value.length === 0) {
throw new Error('Config must include a non-empty "downstreams" array.');
}

for (const entry of value) {
if (!entry || typeof entry !== 'object') {
throw new Error('Invalid downstream entry.');
}

const { id, command, args, env, cwd } = entry as DownstreamConfig;
if (!id || typeof id !== 'string') {
throw new Error('Each downstream requires a string "id".');
}

if (!command || typeof command !== 'string') {
throw new Error(`Downstream "${id}" is missing a "command".`);
}

if (args && !Array.isArray(args)) {
throw new Error(`Downstream "${id}" has invalid "args"; expected an array.`);
}

if (env && typeof env !== 'object') {
throw new Error(`Downstream "${id}" has invalid "env"; expected an object.`);
}

if (cwd && typeof cwd !== 'string') {
throw new Error(`Downstream "${id}" has invalid "cwd"; expected a string.`);
}
}
}

async function readConfig(configPath: string): Promise<CodeModeConfig> {
const resolved = path.resolve(process.cwd(), configPath);
const raw = await readFile(resolved, 'utf8');
const parsed = JSON.parse(raw);

assertDownstreamConfig(parsed.downstreams);

return {
server: parsed.server,
downstreams: parsed.downstreams
};
}

function printUsage(): void {
console.log('Usage: npm run code-mode -- --config ./code-mode.config.json');
}

async function main(): Promise<void> {
const configPath = parseArgs(process.argv.slice(2));
if (!configPath) {
printUsage();
process.exitCode = 1;
return;
}

const config = await readConfig(configPath);
const wrapper = new CodeModeWrapper({
serverInfo: config.server,
downstreams: config.downstreams
});

const transport = new StdioServerTransport();
await wrapper.connect(transport);
console.log('Code Mode wrapper is running on stdio.');

let shuttingDown = false;
const shutdown = async () => {
if (shuttingDown) {
return;
}

shuttingDown = true;
await wrapper.close();
};

process.on('SIGINT', () => {
void shutdown().finally(() => process.exit(0));
});

process.on('SIGTERM', () => {
void shutdown().finally(() => process.exit(0));
});
}

main().catch(error => {
console.error(error);
process.exit(1);
});
112 changes: 112 additions & 0 deletions src/code-mode/downstream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Client } from '../client/index.js';
import { StdioClientTransport, getDefaultEnvironment } from '../client/stdio.js';
import { CallToolResultSchema, ToolListChangedNotificationSchema } from '../types.js';
import type { CallToolResult, Implementation, Tool } from '../types.js';

export type DownstreamConfig = {
id: string;
command: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
description?: string;
};

export interface DownstreamHandle {
readonly config: DownstreamConfig;
listTools(): Promise<Tool[]>;
getTool(toolName: string): Promise<Tool | undefined>;
callTool(toolName: string, args?: Record<string, unknown>): Promise<CallToolResult>;
close(): Promise<void>;
}

export class DefaultDownstreamHandle implements DownstreamHandle {
private readonly _clientInfo: Implementation;
private _client?: Client;
private _toolsCache?: Tool[];
private _listPromise?: Promise<Tool[]>;

constructor(
private readonly _config: DownstreamConfig,
clientInfo: Implementation
) {
this._clientInfo = clientInfo;
}

get config(): DownstreamConfig {
return this._config;
}

async listTools(): Promise<Tool[]> {
if (this._toolsCache) {
return this._toolsCache;
}

if (this._listPromise) {
return this._listPromise;
}

this._listPromise = this._ensureClient()
.then(async client => {
const result = await client.listTools();
this._toolsCache = result.tools;
return this._toolsCache;
})
.finally(() => {
this._listPromise = undefined;
});

return this._listPromise;
}

async getTool(toolName: string): Promise<Tool | undefined> {
const tools = await this.listTools();
return tools.find(tool => tool.name === toolName);
}

async callTool(toolName: string, args?: Record<string, unknown>): Promise<CallToolResult> {
const client = await this._ensureClient();
return client.callTool(
{
name: toolName,
arguments: args
},
CallToolResultSchema
) as Promise<CallToolResult>;
}

async close(): Promise<void> {
await this._client?.close();
this._client = undefined;
this._toolsCache = undefined;
}

private async _ensureClient(): Promise<Client> {
if (this._client) {
return this._client;
}

const transport = new StdioClientTransport({
command: this._config.command,
args: this._config.args,
env: {
...getDefaultEnvironment(),
...this._config.env
},
cwd: this._config.cwd
});

const client = new Client({
name: `code-mode:${this._config.id}`,
version: '0.1.0'
});

await client.connect(transport);
client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
this._toolsCache = undefined;
});

this._client = client;
return client;
}
}
13 changes: 13 additions & 0 deletions src/code-mode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export { CodeModeWrapper, type CodeModeWrapperOptions } from './wrapper.js';
export type { DownstreamConfig, DownstreamHandle } from './downstream.js';
export {
ListToolNamesInputSchema,
ListToolNamesOutputSchema,
type ListToolNamesResult,
type ToolSummary,
GetToolImplementationInputSchema,
GetToolImplementationOutputSchema,
type GetToolImplementationResult,
CallToolInputSchema,
type CallToolInput
} from './metaTools.js';
54 changes: 54 additions & 0 deletions src/code-mode/metaTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { z } from 'zod';

export const ServerSummarySchema = z.object({
serverId: z.string(),
description: z.string().optional()
});

export const ListMcpServersOutputSchema = z.object({
servers: z.array(ServerSummarySchema)
});

export type ListMcpServersResult = z.infer<typeof ListMcpServersOutputSchema>;

export const ToolSummarySchema = z.object({
serverId: z.string(),
toolName: z.string(),
description: z.string().optional()
});

export const ListToolNamesInputSchema = z.object({
serverId: z.string()
});

export const ListToolNamesOutputSchema = z.object({
tools: z.array(ToolSummarySchema)
});

export type ToolSummary = z.infer<typeof ToolSummarySchema>;
export type ListToolNamesResult = z.infer<typeof ListToolNamesOutputSchema>;

export const GetToolImplementationInputSchema = z.object({
serverId: z.string(),
toolName: z.string()
});

export const GetToolImplementationOutputSchema = z.object({
serverId: z.string(),
toolName: z.string(),
signature: z.string(),
description: z.string().optional(),
annotations: z.record(z.unknown()).optional(),
inputSchema: z.record(z.unknown()).optional(),
outputSchema: z.record(z.unknown()).optional()
});

export type GetToolImplementationResult = z.infer<typeof GetToolImplementationOutputSchema>;

export const CallToolInputSchema = z.object({
serverId: z.string(),
toolName: z.string(),
arguments: z.record(z.unknown()).optional()
});

export type CallToolInput = z.infer<typeof CallToolInputSchema>;
Loading
Loading