Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Closed
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 @@ -83,6 +83,7 @@ Options:
-rr, --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 @@ -46,6 +46,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`

@pelikhan pelikhan Sep 13, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot also support ${VARIABLE_NAME} to resolve a env variable (must be capitalized)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added support for ${VARIABLE_NAME} syntax to resolve capitalized environment variables in addition to the existing ${env:VARIABLE_NAME} format. Updated both code and documentation. Changes in commit 064492a.

- `${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
4 changes: 4 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ export async function cli() {
)
.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
142 changes: 142 additions & 0 deletions packages/cli/src/mcp-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { loadClaudeMcpConfig } from "../src/mcp-config"
import { writeJSON, readJSON } from "fs-extra"
import { resolve } from "node:path"
import { tmpdir } from "node:os"
import { mkdtemp, rm } from "node:fs/promises"

describe("MCP Configuration Loading", () => {
let tempDir: string

beforeEach(async () => {
tempDir = await mkdtemp(resolve(tmpdir(), "genaiscript-mcp-test-"))
})

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true })
})

test("should load basic MCP configuration", async () => {
const configPath = resolve(tempDir, "mcp.json")
const config = {
servers: {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem"]
},
memory: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-memory"]
}
}
}

await writeJSON(configPath, config)
const result = await loadClaudeMcpConfig(configPath)

expect(result).toEqual({
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem"],
env: undefined,
cwd: undefined
},
memory: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-memory"],
env: undefined,
cwd: undefined
}
})
})

test("should interpolate workspaceFolder variable", async () => {
const configPath = resolve(tempDir, "mcp.json")
const workspaceFolder = "/test/workspace"
const config = {
servers: {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "${workspaceFolder}"]
}
}
}

await writeJSON(configPath, config)
const result = await loadClaudeMcpConfig(configPath, workspaceFolder)

expect(result.filesystem.args).toEqual([
"-y",
"@modelcontextprotocol/server-filesystem",
workspaceFolder
])
})

test("should interpolate environment variables", async () => {
const configPath = resolve(tempDir, "mcp.json")
const config = {
servers: {
test: {
command: "test",
env: {
"DEBUG": "${env:TEST_DEBUG}",
"PATH": "${env:PATH}"
}
}
}
}

// Set test environment variable
process.env.TEST_DEBUG = "true"

await writeJSON(configPath, config)
const result = await loadClaudeMcpConfig(configPath)

expect(result.test.env.DEBUG).toBe("true")
expect(result.test.env.PATH).toBe(process.env.PATH)
})

test("should handle missing configuration file", async () => {
const nonExistentPath = resolve(tempDir, "missing.json")

await expect(loadClaudeMcpConfig(nonExistentPath)).rejects.toThrow(
/MCP configuration file not found/
)
})

test("should handle invalid JSON", async () => {
const configPath = resolve(tempDir, "invalid.json")
await writeJSON(configPath, "invalid json content")

await expect(loadClaudeMcpConfig(configPath)).rejects.toThrow(
/Failed to parse MCP configuration file/
)
})

test("should handle missing servers object", async () => {
const configPath = resolve(tempDir, "no-servers.json")
const config = { other: "data" }

await writeJSON(configPath, config)

await expect(loadClaudeMcpConfig(configPath)).rejects.toThrow(
/Invalid MCP configuration: missing or invalid 'servers' object/
)
})

test("should use config file directory as default workspace folder", async () => {
const configPath = resolve(tempDir, "mcp.json")
const config = {
servers: {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "${workspaceFolder}"]
}
}
}

await writeJSON(configPath, config)
const result = await loadClaudeMcpConfig(configPath)

expect(result.filesystem.args[2]).toBe(tempDir)
})
})
126 changes: 126 additions & 0 deletions packages/cli/src/mcp-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { readJSON } from "fs-extra"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use genaiscriptDebug and add debug statements in the file to trace the mcp server resolution

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added genaiscriptDebug logging throughout the MCP configuration loading process to trace server resolution, including config file parsing, workspace folder detection, server processing, and final server count. Changes in commit 064492a.

import { resolve, dirname } from "node:path"
import { existsSync } from "node:fs"
import { genaiscriptDebug } from "../../core/src/debug"

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
}
19 changes: 19 additions & 0 deletions packages/cli/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import { genaiscriptDebug } from "../../core/src/debug"
import { uriTryParse } from "../../core/src/url"
import { tryResolveScript } from "../../core/src/scriptresolver"
import { isCI } from "../../core/src/ci"
import { loadClaudeMcpConfig } from "./mcp-config"
const dbg = genaiscriptDebug("run")

/**
Expand Down Expand Up @@ -569,6 +570,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) {
script.mcpServers = { ...script.mcpServers, ...mcpServers }
trace.item("Loading MCP servers from configuration", JSON.stringify(Object.keys(mcpServers)))
}
} 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
Loading
Loading