mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
* fix: merge multiple --mcp-config flags instead of overwriting When users provide their own --mcp-config in claude_args, the action's built-in MCP servers (github_comment, github_ci, etc.) were being lost because multiple --mcp-config flags were overwriting each other. This fix: - Adds mcp-config to ACCUMULATING_FLAGS to collect all values - Changes delimiter to null character to avoid conflicts with JSON - Adds mergeMcpConfigs() to combine mcpServers objects from multiple configs - Merges inline JSON configs while preserving file path configs Fixes #745 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ashwin Bhat <ashwin-ant@users.noreply.github.com> * fix: support hyphenated --allowed-tools flag and multiple values The --allowed-tools flag was not being parsed correctly when: 1. Using the hyphenated form (--allowed-tools) instead of camelCase (--allowedTools) 2. Passing multiple space-separated values after a single flag (e.g., --allowed-tools "Tool1" "Tool2" "Tool3") This fix: - Adds hyphenated variants (allowed-tools, disallowed-tools) to ACCUMULATING_FLAGS - Updates parsing to consume all consecutive non-flag values for accumulating flags - Merges values from both camelCase and hyphenated variants Fixes #746 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ashwin Bhat <ashwin-ant@users.noreply.github.com>
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { describe, test, expect } from "bun:test";
|
|
import { parseSdkOptions } from "../src/parse-sdk-options";
|
|
import type { ClaudeOptions } from "../src/run-claude";
|
|
|
|
describe("parseSdkOptions", () => {
|
|
describe("allowedTools merging", () => {
|
|
test("should extract allowedTools from claudeArgs", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--allowedTools "Edit,Read,Write"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
|
expect(result.sdkOptions.extraArgs?.["allowedTools"]).toBeUndefined();
|
|
});
|
|
|
|
test("should extract allowedTools from claudeArgs with MCP tools", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs:
|
|
'--allowedTools "Edit,Read,mcp__github_comment__update_claude_comment"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual([
|
|
"Edit",
|
|
"Read",
|
|
"mcp__github_comment__update_claude_comment",
|
|
]);
|
|
});
|
|
|
|
test("should accumulate multiple --allowedTools flags from claudeArgs", () => {
|
|
// This simulates tag mode adding its tools, then user adding their own
|
|
const options: ClaudeOptions = {
|
|
claudeArgs:
|
|
'--allowedTools "Edit,Read,mcp__github_comment__update_claude_comment" --model "claude-3" --allowedTools "Bash(npm install),mcp__github__get_issue"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual([
|
|
"Edit",
|
|
"Read",
|
|
"mcp__github_comment__update_claude_comment",
|
|
"Bash(npm install)",
|
|
"mcp__github__get_issue",
|
|
]);
|
|
});
|
|
|
|
test("should merge allowedTools from both claudeArgs and direct options", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--allowedTools "Edit,Read"',
|
|
allowedTools: "Write,Glob",
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual([
|
|
"Edit",
|
|
"Read",
|
|
"Write",
|
|
"Glob",
|
|
]);
|
|
});
|
|
|
|
test("should deduplicate allowedTools when merging", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--allowedTools "Edit,Read"',
|
|
allowedTools: "Edit,Write",
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
|
});
|
|
|
|
test("should use only direct options when claudeArgs has no allowedTools", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--model "claude-3-5-sonnet"',
|
|
allowedTools: "Edit,Read",
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read"]);
|
|
});
|
|
|
|
test("should return undefined allowedTools when neither source has it", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--model "claude-3-5-sonnet"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toBeUndefined();
|
|
});
|
|
|
|
test("should remove allowedTools from extraArgs after extraction", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--allowedTools "Edit,Read" --model "claude-3-5-sonnet"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.extraArgs?.["allowedTools"]).toBeUndefined();
|
|
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-3-5-sonnet");
|
|
});
|
|
|
|
test("should handle hyphenated --allowed-tools flag", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--allowed-tools "Edit,Read,Write"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
|
expect(result.sdkOptions.extraArgs?.["allowed-tools"]).toBeUndefined();
|
|
});
|
|
|
|
test("should accumulate multiple --allowed-tools flags (hyphenated)", () => {
|
|
// This is the exact scenario from issue #746
|
|
const options: ClaudeOptions = {
|
|
claudeArgs:
|
|
'--allowed-tools "Bash(git log:*)" "Bash(git diff:*)" "Bash(git fetch:*)" "Bash(gh pr:*)"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.allowedTools).toEqual([
|
|
"Bash(git log:*)",
|
|
"Bash(git diff:*)",
|
|
"Bash(git fetch:*)",
|
|
"Bash(gh pr:*)",
|
|
]);
|
|
});
|
|
|
|
test("should handle mixed camelCase and hyphenated allowedTools flags", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--allowedTools "Edit,Read" --allowed-tools "Write,Glob"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
// Both should be merged - note: order depends on which key is found first
|
|
expect(result.sdkOptions.allowedTools).toContain("Edit");
|
|
expect(result.sdkOptions.allowedTools).toContain("Read");
|
|
expect(result.sdkOptions.allowedTools).toContain("Write");
|
|
expect(result.sdkOptions.allowedTools).toContain("Glob");
|
|
});
|
|
});
|
|
|
|
describe("disallowedTools merging", () => {
|
|
test("should extract disallowedTools from claudeArgs", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--disallowedTools "Bash,Write"',
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.disallowedTools).toEqual(["Bash", "Write"]);
|
|
expect(result.sdkOptions.extraArgs?.["disallowedTools"]).toBeUndefined();
|
|
});
|
|
|
|
test("should merge disallowedTools from both sources", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: '--disallowedTools "Bash"',
|
|
disallowedTools: "Write",
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.disallowedTools).toEqual(["Bash", "Write"]);
|
|
});
|
|
});
|
|
|
|
describe("mcp-config merging", () => {
|
|
test("should pass through single mcp-config in extraArgs", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--mcp-config '{"mcpServers":{"server1":{"command":"cmd1"}}}'`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.extraArgs?.["mcp-config"]).toBe(
|
|
'{"mcpServers":{"server1":{"command":"cmd1"}}}',
|
|
);
|
|
});
|
|
|
|
test("should merge multiple mcp-config flags with inline JSON", () => {
|
|
// Simulates action prepending its config, then user providing their own
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--mcp-config '{"mcpServers":{"github_comment":{"command":"node","args":["server.js"]}}}' --mcp-config '{"mcpServers":{"user_server":{"command":"custom","args":["run"]}}}'`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
const mcpConfig = JSON.parse(
|
|
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
|
);
|
|
expect(mcpConfig.mcpServers).toHaveProperty("github_comment");
|
|
expect(mcpConfig.mcpServers).toHaveProperty("user_server");
|
|
expect(mcpConfig.mcpServers.github_comment.command).toBe("node");
|
|
expect(mcpConfig.mcpServers.user_server.command).toBe("custom");
|
|
});
|
|
|
|
test("should merge three mcp-config flags", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--mcp-config '{"mcpServers":{"server1":{"command":"cmd1"}}}' --mcp-config '{"mcpServers":{"server2":{"command":"cmd2"}}}' --mcp-config '{"mcpServers":{"server3":{"command":"cmd3"}}}'`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
const mcpConfig = JSON.parse(
|
|
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
|
);
|
|
expect(mcpConfig.mcpServers).toHaveProperty("server1");
|
|
expect(mcpConfig.mcpServers).toHaveProperty("server2");
|
|
expect(mcpConfig.mcpServers).toHaveProperty("server3");
|
|
});
|
|
|
|
test("should handle mcp-config file path when no inline JSON exists", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--mcp-config /tmp/user-mcp-config.json`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.extraArgs?.["mcp-config"]).toBe(
|
|
"/tmp/user-mcp-config.json",
|
|
);
|
|
});
|
|
|
|
test("should merge inline JSON configs when file path is also present", () => {
|
|
// When action provides inline JSON and user provides a file path,
|
|
// the inline JSON configs should be merged (file paths cannot be merged at parse time)
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--mcp-config '{"mcpServers":{"github_comment":{"command":"node"}}}' --mcp-config '{"mcpServers":{"github_ci":{"command":"node"}}}' --mcp-config /tmp/user-config.json`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
// The inline JSON configs should be merged
|
|
const mcpConfig = JSON.parse(
|
|
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
|
);
|
|
expect(mcpConfig.mcpServers).toHaveProperty("github_comment");
|
|
expect(mcpConfig.mcpServers).toHaveProperty("github_ci");
|
|
});
|
|
|
|
test("should handle mcp-config with other flags", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--mcp-config '{"mcpServers":{"server1":{}}}' --model claude-3-5-sonnet --mcp-config '{"mcpServers":{"server2":{}}}'`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
const mcpConfig = JSON.parse(
|
|
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
|
);
|
|
expect(mcpConfig.mcpServers).toHaveProperty("server1");
|
|
expect(mcpConfig.mcpServers).toHaveProperty("server2");
|
|
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-3-5-sonnet");
|
|
});
|
|
|
|
test("should handle real-world scenario: action config + user config", () => {
|
|
// This is the exact scenario from the bug report
|
|
const actionConfig = JSON.stringify({
|
|
mcpServers: {
|
|
github_comment: {
|
|
command: "node",
|
|
args: ["github-comment-server.js"],
|
|
},
|
|
github_ci: { command: "node", args: ["github-ci-server.js"] },
|
|
},
|
|
});
|
|
const userConfig = JSON.stringify({
|
|
mcpServers: {
|
|
my_custom_server: { command: "python", args: ["server.py"] },
|
|
},
|
|
});
|
|
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--mcp-config '${actionConfig}' --mcp-config '${userConfig}'`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
const mcpConfig = JSON.parse(
|
|
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
|
);
|
|
// All servers should be present
|
|
expect(mcpConfig.mcpServers).toHaveProperty("github_comment");
|
|
expect(mcpConfig.mcpServers).toHaveProperty("github_ci");
|
|
expect(mcpConfig.mcpServers).toHaveProperty("my_custom_server");
|
|
});
|
|
});
|
|
|
|
describe("other extraArgs passthrough", () => {
|
|
test("should pass through json-schema in extraArgs", () => {
|
|
const options: ClaudeOptions = {
|
|
claudeArgs: `--json-schema '{"type":"object"}'`,
|
|
};
|
|
|
|
const result = parseSdkOptions(options);
|
|
|
|
expect(result.sdkOptions.extraArgs?.["json-schema"]).toBe(
|
|
'{"type":"object"}',
|
|
);
|
|
expect(result.hasJsonSchema).toBe(true);
|
|
});
|
|
});
|
|
});
|