Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
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
1 change: 1 addition & 0 deletions docs/src/content/docs/reference/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Options:
--run-retry <number> number of retries for the entire run
--no-run-trace disable automatic trace generation
--no-output-trace disable automatic output generation
--mcp-config <file> MCP configuration file (Claude format) to load servers from
-h, --help display help for command
```

Expand Down
59 changes: 59 additions & 0 deletions docs/src/content/docs/reference/scripts/mcp-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,65 @@ See [MCP server](/genaiscript/reference/scripts/mcp-server) for more details.

:::

## CLI MCP Configuration

### Using MCP configuration files

You can also load MCP servers from a Claude format configuration file using the `--mcp-config` option when running scripts:

```bash
genaiscript run my-script --mcp-config .vscode/mcp.json
```

The configuration file uses the Claude MCP format and supports both `servers` and `mcpServers` as the top-level key:

```json title="mcp.json"
{
"mcpServers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "${workspaceFolder}"],
"env": {
"DEBUG": "${env:DEBUG}"
}
},
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"]
}
}
}
```

### Environment Variable Interpolation

The configuration file supports Claude environment variable interpolation syntax:

- `${workspaceFolder}` - Resolves to the workspace folder (or the directory containing the config file)
- `${env:VARIABLE_NAME}` - Resolves to the value of the environment variable `VARIABLE_NAME`
- `${VARIABLE_NAME}` - Resolves to the value of the environment variable `VARIABLE_NAME` (for capitalized variables)

```json title="Example with environment variables"
{
"servers": {
"custom-server": {
"command": "${env:MCP_SERVER_PATH}",
"args": ["--port", "${MCP_PORT}"],
"cwd": "${workspaceFolder}/servers",
"env": {
"DEBUG": "${env:DEBUG}",
"API_KEY": "${API_KEY}"
}
}
}
}
```

### Combining with Script Configuration

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.

## Configuring servers

You can declare the MCP server configuration in the `script` function (as tools or agents)
Expand Down
19 changes: 19 additions & 0 deletions packages/api/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import {
resolveRuntimeHost,
} from "@genaiscript/core";
import { fileURLToPath } from "node:url";
import { loadClaudeMcpConfig } from "@genaiscript/core";

const dbg = genaiscriptDebug("run");

Expand Down Expand Up @@ -488,6 +489,24 @@ export async function runScriptInternal(
);
}

// Load MCP configuration if provided
if (options.mcpConfig) {
try {
const mcpServers = await loadClaudeMcpConfig(options.mcpConfig, process.cwd());
// Merge MCP servers into the script configuration
if (Object.keys(mcpServers).length > 0) {
const existingServers =
(typeof script.mcpServers === "object" && script.mcpServers) || {};
script.mcpServers = { ...existingServers, ...mcpServers };
trace.item("Loading MCP servers from configuration");
trace.item(`servers: ${Object.keys(mcpServers).join(", ")}`);
}
} catch (error) {
trace.error(undefined, `Failed to load MCP configuration: ${error.message}`);
return fail(`Failed to load MCP configuration: ${error.message}`, CONFIGURATION_ERROR_CODE);
}
}

result = await runTemplate(prj, script, fragment, {
runId,
inner: false,
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,7 @@ export async function cli(): Promise<void> {
"--fallback-tools",
"Enable prompt-based tools instead of builtin LLM tool calling builtin tool calls",
)
.option(
"--mcps <string>",
"path to MCP configuration file to override the script's MCP list",
)
.option("--mcps <string>", "path to MCP configuration file to override the script's MCP list")
.option(
"-o, --out <string>",
"output folder. Extra markdown fields for output and trace will also be generated",
Expand Down Expand Up @@ -250,6 +247,7 @@ export async function cli(): Promise<void> {
.option("--run-retry <number>", "number of retries for the entire run")
.option("--no-run-trace", "disable automatic trace generation")
.option("--no-output-trace", "disable automatic output generation")
.option("--mcp-config <file>", "MCP configuration file (Claude format) to load servers from")
.action(runScriptWithExitCode); // Action to execute the script with exit code

// runs commands
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export * from "./logging.js";
export * from "./logprob.js";
export * from "./markdown.js";
export * from "./math.js";
export * from "./mcp-config.js";
export * from "./mcpclient.js";
export * from "./mcpresource.js";
export * from "./mcpsampling.js";
Expand Down
128 changes: 128 additions & 0 deletions packages/core/src/mcp-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { readJSON } from "./fs.js";
import { resolve, dirname } from "node:path";
import { existsSync } from "node:fs";
import { genaiscriptDebug } from "./debug.js";

const dbg = genaiscriptDebug("mcp:config");

/**
* Claude MCP configuration file format
*/
interface ClaudeMcpConfig {
servers?: Record<string, ClaudeMcpServerConfig>;
mcpServers?: Record<string, ClaudeMcpServerConfig>;
}

interface ClaudeMcpServerConfig {
type?: "stdio";
command: string;
args?: string[];
env?: Record<string, string>;
envFile?: string;
cwd?: string;
}

/**
* Interpolates Claude environment variables in a string
* Supports ${workspaceFolder}, ${env:VARIABLE_NAME}, ${VARIABLE_NAME} (for capitalized env vars), etc.
*/
function interpolateClaudeVariables(
value: string,
workspaceFolder: string,
env: Record<string, string> = process.env,
): string {
return value
.replace(/\$\{workspaceFolder\}/g, workspaceFolder)
.replace(/\$\{env:([^}]+)\}/g, (_, varName) => env[varName] || "")
.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (_, varName) => env[varName] || "");
}

/**
* Recursively interpolates Claude variables in an object
*/
function interpolateObjectValues(
obj: any,
workspaceFolder: string,
env: Record<string, string> = process.env,
): any {
if (typeof obj === "string") {
return interpolateClaudeVariables(obj, workspaceFolder, env);
}
if (Array.isArray(obj)) {
return obj.map((item) => interpolateObjectValues(item, workspaceFolder, env));
}
if (obj && typeof obj === "object") {
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = interpolateObjectValues(value, workspaceFolder, env);
}
return result;
}
return obj;
}

/**
* Loads and parses a Claude MCP configuration file
* @param configPath Path to the MCP configuration file
* @param workspaceFolder Workspace folder for variable interpolation (defaults to config file directory)
* @returns Parsed MCP server configurations
*/
export async function loadClaudeMcpConfig(
configPath: string,
workspaceFolder?: string,
): Promise<Record<string, any>> {
const resolvedPath = resolve(configPath);

dbg(`Loading MCP configuration from: ${resolvedPath}`);

if (!existsSync(resolvedPath)) {
throw new Error(`MCP configuration file not found: ${resolvedPath}`);
}

let config: ClaudeMcpConfig;
try {
config = await readJSON(resolvedPath);
dbg(`Successfully parsed MCP configuration file`);
} catch (error) {
dbg(`Failed to parse MCP configuration file: ${error.message}`);
throw new Error(`Failed to parse MCP configuration file: ${error.message}`);
}

// Support both "servers" and "mcpServers" key names
const serversConfig = config.servers || config.mcpServers;
if (!serversConfig || typeof serversConfig !== "object") {
throw new Error(
"Invalid MCP configuration: missing or invalid 'servers' or 'mcpServers' object",
);
}

// Use config file directory as workspace folder if not provided
const wsFolder = workspaceFolder || dirname(resolvedPath);
dbg(`Using workspace folder: ${wsFolder}`);

// Convert Claude format to GenAIScript format
const mcpServers: Record<string, any> = {};

for (const [serverId, serverConfig] of Object.entries(serversConfig)) {
dbg(`Processing server: ${serverId}`);

// Interpolate variables in the server configuration
const interpolatedConfig = interpolateObjectValues(serverConfig, wsFolder);

dbg(`Interpolated config for ${serverId}:`, interpolatedConfig);

// Convert to GenAIScript McpServerConfig format
const genaiscriptConfig = {
command: interpolatedConfig.command,
args: interpolatedConfig.args || [],
env: interpolatedConfig.env,
cwd: interpolatedConfig.cwd,
};

mcpServers[serverId] = genaiscriptConfig;
}

dbg(`Loaded ${Object.keys(mcpServers).length} MCP servers:`, Object.keys(mcpServers));

return mcpServers;
}
1 change: 1 addition & 0 deletions packages/core/src/server/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export interface PromptScriptRunOptions {
outputTrace: boolean;
accept: string;
mcps: string;
mcpConfig?: string;
}

export interface RunResultList extends RequestMessage {
Expand Down
Loading