Skip to content

Commit 1a2d7f8

Browse files
authored
Merge pull request #29 from mcpc-tech/feat/cli-add-proxy-mode
feat: implement proxy configuration and mode handling in CLI
2 parents 021ec78 + 5006d90 commit 1a2d7f8

File tree

5 files changed

+247
-18
lines changed

5 files changed

+247
-18
lines changed

packages/cli/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"./app": "./src/app.ts"
1313
},
1414
"imports": {
15+
"@mcpc-tech/plugin-code-execution": "npm:@mcpc-tech/plugin-code-execution@^0.0.3",
1516
"@mcpc/core": "jsr:@mcpc/core@^0.3.4",
1617
"@mcpc/utils": "jsr:@mcpc/utils@^0.2.2",
1718
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.8.0",

packages/cli/src/bin.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,19 @@
3838
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3939
import { createServer } from "./app.ts";
4040
import { loadConfig } from "./config/loader.ts";
41+
import { createCodeExecutionPlugin } from "@mcpc-tech/plugin-code-execution";
4142

4243
// Load configuration from environment or file
4344
const config = await loadConfig();
4445

46+
// Add plugins
47+
config?.agents.forEach((agent) => {
48+
if (agent.plugins?.length ?? 0 === 0) {
49+
agent.plugins = [];
50+
}
51+
agent.plugins?.push(createCodeExecutionPlugin());
52+
});
53+
4554
if (config) {
4655
console.error(`Loaded configuration with ${config.agents.length} agent(s)`);
4756
} else {

packages/cli/src/config/loader.ts

Lines changed: 171 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,99 @@ export interface MCPCConfig {
7070
agents: ComposeDefinition[];
7171
}
7272

73+
/**
74+
* Create proxy configuration from command-line arguments
75+
* This generates an MCPC config that wraps an existing MCP server
76+
*/
77+
function createProxyConfig(args: {
78+
transportType?: string;
79+
proxyCommand?: string[];
80+
mode?: string;
81+
}): MCPCConfig {
82+
if (!args.proxyCommand || args.proxyCommand.length === 0) {
83+
console.error("Error: --proxy requires a command after --");
84+
console.error(
85+
"Example: mcpc --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander",
86+
);
87+
process.exit(1);
88+
}
89+
90+
if (!args.transportType) {
91+
console.error("Error: --proxy requires --transport-type to be specified");
92+
console.error("Supported types: stdio, streamable-http, sse");
93+
console.error(
94+
"Example: mcpc --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander",
95+
);
96+
process.exit(1);
97+
}
98+
99+
const validTransports = ["stdio", "streamable-http", "sse"];
100+
if (!validTransports.includes(args.transportType)) {
101+
console.error(`Error: Invalid transport type '${args.transportType}'`);
102+
console.error(`Supported types: ${validTransports.join(", ")}`);
103+
process.exit(1);
104+
}
105+
106+
const command = args.proxyCommand[0];
107+
const commandArgs = args.proxyCommand.slice(1);
108+
109+
// Extract server name from command (e.g., "@wonderwhy-er/desktop-commander" -> "desktop-commander")
110+
let serverName = "mcp-server";
111+
const npmPackageMatch = command.match(/@[\w-]+\/([\w-]+)/) ||
112+
commandArgs.join(" ").match(/@[\w-]+\/([\w-]+)/);
113+
if (npmPackageMatch) {
114+
serverName = npmPackageMatch[1];
115+
} else {
116+
// Try to get name from command itself
117+
const baseName = command.split("/").pop()?.replace(/\.js$/, "");
118+
if (baseName && baseName !== "npx" && baseName !== "node") {
119+
serverName = baseName;
120+
}
121+
}
122+
123+
// Create configuration
124+
const config: MCPCConfig = {
125+
name: `${serverName}-proxy`,
126+
version: "0.1.0",
127+
capabilities: {
128+
tools: {},
129+
sampling: {},
130+
},
131+
agents: [
132+
{
133+
name: serverName,
134+
description:
135+
`Agentic tool to orchestrate ${serverName} MCP server tools:
136+
<tool name="${serverName}.__ALL__"/>`,
137+
deps: {
138+
mcpServers: {
139+
[serverName]: {
140+
command: command,
141+
args: commandArgs,
142+
transportType: args.transportType as
143+
| "stdio"
144+
| "streamable-http"
145+
| "sse",
146+
},
147+
},
148+
},
149+
options: {
150+
mode: (args.mode || "agentic") as any,
151+
},
152+
},
153+
],
154+
};
155+
156+
console.error(`Created proxy configuration for ${serverName}`);
157+
console.error(`Transport: ${args.transportType}`);
158+
console.error(`Command: ${command} ${commandArgs.join(" ")}`);
159+
if (args.mode) {
160+
console.error(`Mode: ${args.mode}`);
161+
}
162+
163+
return config;
164+
}
165+
73166
/**
74167
* Print help message
75168
*/
@@ -78,7 +171,7 @@ function printHelp(): void {
78171
MCPC CLI - Model Context Protocol Composer
79172
80173
USAGE:
81-
npx -y deno run -A jsr:@mcpc/cli/bin [OPTIONS]
174+
mcpc [OPTIONS]
82175
83176
OPTIONS:
84177
--help, -h Show this help message
@@ -89,6 +182,18 @@ OPTIONS:
89182
Add custom HTTP header for URL fetching
90183
Format: "Key: Value" or "Key=Value"
91184
Can be used multiple times
185+
--mode <mode> Set execution mode for all agents
186+
Supported modes:
187+
- agentic: Fully autonomous agent mode (default)
188+
- agentic_workflow: Agent workflow mode with dynamic or predefined steps
189+
- agentic_sampling: Autonomous sampling mode for agentic execution
190+
- agentic_workflow_sampling: Autonomous sampling mode for workflow execution
191+
- code_execution: Code execution mode for most efficient token usage
192+
--proxy Proxy mode: automatically configure MCPC to wrap an MCP server
193+
Use with --transport-type to specify the transport
194+
Example: --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander
195+
--transport-type <type> Transport type for proxy mode
196+
Supported types: stdio, streamable-http, sse
92197
93198
ENVIRONMENT VARIABLES:
94199
MCPC_CONFIG Inline JSON configuration (same as --config)
@@ -97,27 +202,39 @@ ENVIRONMENT VARIABLES:
97202
98203
EXAMPLES:
99204
# Show help
100-
npx -y deno run -A jsr:@mcpc/cli/bin --help
205+
mcpc --help
206+
207+
# Proxy mode - wrap an existing MCP server (stdio)
208+
mcpc --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander
209+
210+
# Proxy mode - wrap an MCP server (streamable-http)
211+
mcpc --proxy --transport-type streamable-http -- https://api.example.com/mcp
212+
213+
# Proxy mode - wrap an MCP server (sse)
214+
mcpc --proxy --transport-type sse -- https://api.example.com/sse
101215
102216
# Load from URL
103-
npx -y deno run -A jsr:@mcpc/cli/bin --config-url \\
217+
mcpc --config-url \\
104218
"https://raw.githubusercontent.com/mcpc-tech/mcpc/main/packages/cli/examples/configs/codex-fork.json"
105219
106220
# Load from URL with custom headers
107-
npx -y deno run -A jsr:@mcpc/cli/bin \\
221+
mcpc \\
108222
--config-url "https://api.example.com/config.json" \\
109223
-H "Authorization: Bearer token123" \\
110224
-H "X-Custom-Header: value"
111225
112226
# Load from file
113-
npx -y deno run -A jsr:@mcpc/cli/bin --config-file ./my-config.json
227+
mcpc --config-file ./my-config.json
228+
229+
# Override execution mode for all agents
230+
mcpc --config-file ./my-config.json --mode agentic_workflow
114231
115232
# Using environment variable
116233
export MCPC_CONFIG='[{"name":"agent","description":"..."}]'
117-
npx -y deno run -A jsr:@mcpc/cli/bin
234+
mcpc
118235
119236
# Use default configuration (./mcpc.config.json)
120-
npx -y deno run -A jsr:@mcpc/cli/bin
237+
mcpc
121238
122239
CONFIGURATION:
123240
Configuration files support environment variable substitution using $VAR_NAME syntax.
@@ -142,6 +259,10 @@ function parseArgs(): {
142259
configFile?: string;
143260
requestHeaders?: Record<string, string>;
144261
help?: boolean;
262+
proxy?: boolean;
263+
transportType?: string;
264+
proxyCommand?: string[];
265+
mode?: string;
145266
} {
146267
const args = process.argv.slice(2);
147268
const result: {
@@ -150,6 +271,10 @@ function parseArgs(): {
150271
configFile?: string;
151272
requestHeaders?: Record<string, string>;
152273
help?: boolean;
274+
proxy?: boolean;
275+
transportType?: string;
276+
proxyCommand?: string[];
277+
mode?: string;
153278
} = {};
154279

155280
for (let i = 0; i < args.length; i++) {
@@ -161,14 +286,15 @@ function parseArgs(): {
161286
} else if (arg === "--config-file" && i + 1 < args.length) {
162287
result.configFile = args[++i];
163288
} else if (
164-
(arg === "--request-headers" || arg === "-H") && i + 1 < args.length
289+
(arg === "--request-headers" || arg === "-H") &&
290+
i + 1 < args.length
165291
) {
166292
// Parse header in format "Key: Value" or "Key=Value"
167293
const headerStr = args[++i];
168294
const colonIdx = headerStr.indexOf(":");
169295
const equalIdx = headerStr.indexOf("=");
170296
const separatorIdx = colonIdx !== -1
171-
? (equalIdx !== -1 ? Math.min(colonIdx, equalIdx) : colonIdx)
297+
? equalIdx !== -1 ? Math.min(colonIdx, equalIdx) : colonIdx
172298
: equalIdx;
173299

174300
if (separatorIdx !== -1) {
@@ -181,6 +307,16 @@ function parseArgs(): {
181307
}
182308
} else if (arg === "--help" || arg === "-h") {
183309
result.help = true;
310+
} else if (arg === "--proxy") {
311+
result.proxy = true;
312+
} else if (arg === "--transport-type" && i + 1 < args.length) {
313+
result.transportType = args[++i];
314+
} else if (arg === "--mode" && i + 1 < args.length) {
315+
result.mode = args[++i];
316+
} else if (arg === "--") {
317+
// Everything after -- is the proxy command
318+
result.proxyCommand = args.slice(i + 1);
319+
break;
184320
}
185321
}
186322

@@ -200,11 +336,16 @@ export async function loadConfig(): Promise<MCPCConfig | null> {
200336
process.exit(0);
201337
}
202338

339+
// Handle --proxy mode
340+
if (args.proxy) {
341+
return createProxyConfig(args);
342+
}
343+
203344
// Priority 1: --config (inline JSON string)
204345
if (args.config) {
205346
try {
206347
const parsed = JSON.parse(args.config);
207-
return normalizeConfig(parsed);
348+
return applyModeOverride(normalizeConfig(parsed), args.mode);
208349
} catch (error) {
209350
console.error("Failed to parse --config argument:", error);
210351
throw error;
@@ -215,7 +356,7 @@ export async function loadConfig(): Promise<MCPCConfig | null> {
215356
if (process.env.MCPC_CONFIG) {
216357
try {
217358
const parsed = JSON.parse(process.env.MCPC_CONFIG);
218-
return normalizeConfig(parsed);
359+
return applyModeOverride(normalizeConfig(parsed), args.mode);
219360
} catch (error) {
220361
console.error("Failed to parse MCPC_CONFIG environment variable:", error);
221362
throw error;
@@ -236,7 +377,7 @@ export async function loadConfig(): Promise<MCPCConfig | null> {
236377
}
237378
const content = await response.text();
238379
const parsed = JSON.parse(content);
239-
return normalizeConfig(parsed);
380+
return applyModeOverride(normalizeConfig(parsed), args.mode);
240381
} catch (error) {
241382
console.error(`Failed to fetch config from ${configUrl}:`, error);
242383
throw error;
@@ -249,7 +390,7 @@ export async function loadConfig(): Promise<MCPCConfig | null> {
249390
try {
250391
const content = await readFile(configFile, "utf-8");
251392
const parsed = JSON.parse(content);
252-
return normalizeConfig(parsed);
393+
return applyModeOverride(normalizeConfig(parsed), args.mode);
253394
} catch (error) {
254395
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
255396
console.error(`Config file not found: ${configFile}`);
@@ -266,16 +407,13 @@ export async function loadConfig(): Promise<MCPCConfig | null> {
266407
try {
267408
const content = await readFile(defaultConfigPath, "utf-8");
268409
const parsed = JSON.parse(content);
269-
return normalizeConfig(parsed);
410+
return applyModeOverride(normalizeConfig(parsed), args.mode);
270411
} catch (error) {
271412
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
272413
// No config file found, this is okay
273414
return null;
274415
} else {
275-
console.error(
276-
`Failed to load config from ${defaultConfigPath}:`,
277-
error,
278-
);
416+
console.error(`Failed to load config from ${defaultConfigPath}:`, error);
279417
throw error;
280418
}
281419
}
@@ -311,6 +449,21 @@ function replaceEnvVarsInConfig(obj: unknown): unknown {
311449
return obj;
312450
}
313451

452+
/**
453+
* Apply mode override to all agents in the configuration
454+
*/
455+
function applyModeOverride(config: MCPCConfig, mode?: string): MCPCConfig {
456+
if (!mode) return config;
457+
458+
// Apply mode to all agents
459+
config.agents.forEach((agent) => {
460+
if (!agent.options) agent.options = {};
461+
agent.options.mode = mode as any;
462+
});
463+
464+
return config;
465+
}
466+
314467
/**
315468
* Normalize configuration to ensure it has the expected structure
316469
* Supports both array format (legacy) and object format (new)

packages/cli/src/server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,22 @@ import { OpenAPIHono } from "@hono/zod-openapi";
3030
import { createApp } from "./app.ts";
3131
import { loadConfig } from "./config/loader.ts";
3232
import process from "node:process";
33+
import { createCodeExecutionPlugin } from "@mcpc-tech/plugin-code-execution";
3334

3435
const port = Number(process.env.PORT || "9000");
3536
const hostname = "0.0.0.0";
3637

3738
// Load configuration from environment or file
3839
const config = await loadConfig();
3940

41+
// Add plugins
42+
config?.agents.forEach((agent) => {
43+
if (agent.plugins?.length ?? 0 === 0) {
44+
agent.plugins = [];
45+
}
46+
agent.plugins?.push(createCodeExecutionPlugin());
47+
});
48+
4049
if (config) {
4150
console.log(`Loaded configuration with ${config.agents.length} agent(s)`);
4251
} else {

0 commit comments

Comments
 (0)