Skip to content

Commit 0881121

Browse files
authored
Add support for Claude MCP configuration files in CLI run command (#1923)
1 parent 51e5d74 commit 0881121

8 files changed

Lines changed: 354 additions & 4 deletions

File tree

docs/src/content/docs/reference/cli/commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Options:
118118
--run-retry <number> number of retries for the entire run
119119
--no-run-trace disable automatic trace generation
120120
--no-output-trace disable automatic output generation
121+
--mcp-config <file> MCP configuration file (Claude format) to load servers from
121122
-h, --help display help for command
122123
```
123124

docs/src/content/docs/reference/scripts/mcp-tools.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,65 @@ See [MCP server](/genaiscript/reference/scripts/mcp-server) for more details.
179179

180180
:::
181181

182+
## CLI MCP Configuration
183+
184+
### Using MCP configuration files
185+
186+
You can also load MCP servers from a Claude format configuration file using the `--mcp-config` option when running scripts:
187+
188+
```bash
189+
genaiscript run my-script --mcp-config .vscode/mcp.json
190+
```
191+
192+
The configuration file uses the Claude MCP format and supports both `servers` and `mcpServers` as the top-level key:
193+
194+
```json title="mcp.json"
195+
{
196+
"mcpServers": {
197+
"filesystem": {
198+
"type": "stdio",
199+
"command": "npx",
200+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "${workspaceFolder}"],
201+
"env": {
202+
"DEBUG": "${env:DEBUG}"
203+
}
204+
},
205+
"memory": {
206+
"command": "npx",
207+
"args": ["-y", "@modelcontextprotocol/server-memory"]
208+
}
209+
}
210+
}
211+
```
212+
213+
### Environment Variable Interpolation
214+
215+
The configuration file supports Claude environment variable interpolation syntax:
216+
217+
- `${workspaceFolder}` - Resolves to the workspace folder (or the directory containing the config file)
218+
- `${env:VARIABLE_NAME}` - Resolves to the value of the environment variable `VARIABLE_NAME`
219+
- `${VARIABLE_NAME}` - Resolves to the value of the environment variable `VARIABLE_NAME` (for capitalized variables)
220+
221+
```json title="Example with environment variables"
222+
{
223+
"servers": {
224+
"custom-server": {
225+
"command": "${env:MCP_SERVER_PATH}",
226+
"args": ["--port", "${MCP_PORT}"],
227+
"cwd": "${workspaceFolder}/servers",
228+
"env": {
229+
"DEBUG": "${env:DEBUG}",
230+
"API_KEY": "${API_KEY}"
231+
}
232+
}
233+
}
234+
}
235+
```
236+
237+
### Combining with Script Configuration
238+
239+
MCP servers loaded from configuration files are merged with any `mcpServers` defined in the script itself. If there are conflicts, the script configuration takes precedence.
240+
182241
## Configuring servers
183242

184243
You can declare the MCP server configuration in the `script` function (as tools or agents)

packages/api/src/run.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import {
104104
resolveRuntimeHost,
105105
} from "@genaiscript/core";
106106
import { fileURLToPath } from "node:url";
107+
import { loadClaudeMcpConfig } from "@genaiscript/core";
107108

108109
const dbg = genaiscriptDebug("run");
109110

@@ -488,6 +489,24 @@ export async function runScriptInternal(
488489
);
489490
}
490491

492+
// Load MCP configuration if provided
493+
if (options.mcpConfig) {
494+
try {
495+
const mcpServers = await loadClaudeMcpConfig(options.mcpConfig, process.cwd());
496+
// Merge MCP servers into the script configuration
497+
if (Object.keys(mcpServers).length > 0) {
498+
const existingServers =
499+
(typeof script.mcpServers === "object" && script.mcpServers) || {};
500+
script.mcpServers = { ...existingServers, ...mcpServers };
501+
trace.item("Loading MCP servers from configuration");
502+
trace.item(`servers: ${Object.keys(mcpServers).join(", ")}`);
503+
}
504+
} catch (error) {
505+
trace.error(undefined, `Failed to load MCP configuration: ${error.message}`);
506+
return fail(`Failed to load MCP configuration: ${error.message}`, CONFIGURATION_ERROR_CODE);
507+
}
508+
}
509+
491510
result = await runTemplate(prj, script, fragment, {
492511
runId,
493512
inner: false,

packages/cli/src/cli.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,7 @@ export async function cli(): Promise<void> {
193193
"--fallback-tools",
194194
"Enable prompt-based tools instead of builtin LLM tool calling builtin tool calls",
195195
)
196-
.option(
197-
"--mcps <string>",
198-
"path to MCP configuration file to override the script's MCP list",
199-
)
196+
.option("--mcps <string>", "path to MCP configuration file to override the script's MCP list")
200197
.option(
201198
"-o, --out <string>",
202199
"output folder. Extra markdown fields for output and trace will also be generated",
@@ -250,6 +247,7 @@ export async function cli(): Promise<void> {
250247
.option("--run-retry <number>", "number of retries for the entire run")
251248
.option("--no-run-trace", "disable automatic trace generation")
252249
.option("--no-output-trace", "disable automatic output generation")
250+
.option("--mcp-config <file>", "MCP configuration file (Claude format) to load servers from")
253251
.action(runScriptWithExitCode); // Action to execute the script with exit code
254252

255253
// runs commands

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export * from "./logging.js";
103103
export * from "./logprob.js";
104104
export * from "./markdown.js";
105105
export * from "./math.js";
106+
export * from "./mcp-config.js";
106107
export * from "./mcpclient.js";
107108
export * from "./mcpresource.js";
108109
export * from "./mcpsampling.js";

packages/core/src/mcp-config.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { readJSON } from "./fs.js";
2+
import { resolve, dirname } from "node:path";
3+
import { existsSync } from "node:fs";
4+
import { genaiscriptDebug } from "./debug.js";
5+
6+
const dbg = genaiscriptDebug("mcp:config");
7+
8+
/**
9+
* Claude MCP configuration file format
10+
*/
11+
interface ClaudeMcpConfig {
12+
servers?: Record<string, ClaudeMcpServerConfig>;
13+
mcpServers?: Record<string, ClaudeMcpServerConfig>;
14+
}
15+
16+
interface ClaudeMcpServerConfig {
17+
type?: "stdio";
18+
command: string;
19+
args?: string[];
20+
env?: Record<string, string>;
21+
envFile?: string;
22+
cwd?: string;
23+
}
24+
25+
/**
26+
* Interpolates Claude environment variables in a string
27+
* Supports ${workspaceFolder}, ${env:VARIABLE_NAME}, ${VARIABLE_NAME} (for capitalized env vars), etc.
28+
*/
29+
function interpolateClaudeVariables(
30+
value: string,
31+
workspaceFolder: string,
32+
env: Record<string, string> = process.env,
33+
): string {
34+
return value
35+
.replace(/\$\{workspaceFolder\}/g, workspaceFolder)
36+
.replace(/\$\{env:([^}]+)\}/g, (_, varName) => env[varName] || "")
37+
.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (_, varName) => env[varName] || "");
38+
}
39+
40+
/**
41+
* Recursively interpolates Claude variables in an object
42+
*/
43+
function interpolateObjectValues(
44+
obj: any,
45+
workspaceFolder: string,
46+
env: Record<string, string> = process.env,
47+
): any {
48+
if (typeof obj === "string") {
49+
return interpolateClaudeVariables(obj, workspaceFolder, env);
50+
}
51+
if (Array.isArray(obj)) {
52+
return obj.map((item) => interpolateObjectValues(item, workspaceFolder, env));
53+
}
54+
if (obj && typeof obj === "object") {
55+
const result: any = {};
56+
for (const [key, value] of Object.entries(obj)) {
57+
result[key] = interpolateObjectValues(value, workspaceFolder, env);
58+
}
59+
return result;
60+
}
61+
return obj;
62+
}
63+
64+
/**
65+
* Loads and parses a Claude MCP configuration file
66+
* @param configPath Path to the MCP configuration file
67+
* @param workspaceFolder Workspace folder for variable interpolation (defaults to config file directory)
68+
* @returns Parsed MCP server configurations
69+
*/
70+
export async function loadClaudeMcpConfig(
71+
configPath: string,
72+
workspaceFolder?: string,
73+
): Promise<Record<string, any>> {
74+
const resolvedPath = resolve(configPath);
75+
76+
dbg(`Loading MCP configuration from: ${resolvedPath}`);
77+
78+
if (!existsSync(resolvedPath)) {
79+
throw new Error(`MCP configuration file not found: ${resolvedPath}`);
80+
}
81+
82+
let config: ClaudeMcpConfig;
83+
try {
84+
config = await readJSON(resolvedPath);
85+
dbg(`Successfully parsed MCP configuration file`);
86+
} catch (error) {
87+
dbg(`Failed to parse MCP configuration file: ${error.message}`);
88+
throw new Error(`Failed to parse MCP configuration file: ${error.message}`);
89+
}
90+
91+
// Support both "servers" and "mcpServers" key names
92+
const serversConfig = config.servers || config.mcpServers;
93+
if (!serversConfig || typeof serversConfig !== "object") {
94+
throw new Error(
95+
"Invalid MCP configuration: missing or invalid 'servers' or 'mcpServers' object",
96+
);
97+
}
98+
99+
// Use config file directory as workspace folder if not provided
100+
const wsFolder = workspaceFolder || dirname(resolvedPath);
101+
dbg(`Using workspace folder: ${wsFolder}`);
102+
103+
// Convert Claude format to GenAIScript format
104+
const mcpServers: Record<string, any> = {};
105+
106+
for (const [serverId, serverConfig] of Object.entries(serversConfig)) {
107+
dbg(`Processing server: ${serverId}`);
108+
109+
// Interpolate variables in the server configuration
110+
const interpolatedConfig = interpolateObjectValues(serverConfig, wsFolder);
111+
112+
dbg(`Interpolated config for ${serverId}:`, interpolatedConfig);
113+
114+
// Convert to GenAIScript McpServerConfig format
115+
const genaiscriptConfig = {
116+
command: interpolatedConfig.command,
117+
args: interpolatedConfig.args || [],
118+
env: interpolatedConfig.env,
119+
cwd: interpolatedConfig.cwd,
120+
};
121+
122+
mcpServers[serverId] = genaiscriptConfig;
123+
}
124+
125+
dbg(`Loaded ${Object.keys(mcpServers).length} MCP servers:`, Object.keys(mcpServers));
126+
127+
return mcpServers;
128+
}

packages/core/src/server/messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export interface PromptScriptRunOptions {
200200
outputTrace: boolean;
201201
accept: string;
202202
mcps: string;
203+
mcpConfig?: string;
203204
}
204205

205206
export interface RunResultList extends RequestMessage {

0 commit comments

Comments
 (0)