@arii/boomtick-mcp 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.mcp/schemas/agent.search_ddgs.json +28 -24
  2. package/.mcp/schemas/agent_search_ddgs.json +31 -0
  3. package/.mcp/schemas/boomtick.health.json +14 -10
  4. package/.mcp/schemas/boomtick_health.json +16 -0
  5. package/.mcp/schemas/github.checkout_branch.json +28 -24
  6. package/.mcp/schemas/github.comment_triage_summary.json +21 -17
  7. package/.mcp/schemas/github.create_issue.json +55 -50
  8. package/.mcp/schemas/github.create_pull_request.json +53 -48
  9. package/.mcp/schemas/github.get_merge_conflict_files.json +21 -17
  10. package/.mcp/schemas/github.get_pr.json +30 -12
  11. package/.mcp/schemas/github.get_pr_diff.json +30 -12
  12. package/.mcp/schemas/github.issue_comment.json +35 -17
  13. package/.mcp/schemas/github.issue_update.json +114 -96
  14. package/.mcp/schemas/github.issue_view.json +30 -12
  15. package/.mcp/schemas/github.open_replacement_pr.json +64 -60
  16. package/.mcp/schemas/github.search_open_prs.json +61 -43
  17. package/.mcp/schemas/github_checkout_branch.json +31 -0
  18. package/.mcp/schemas/github_comment_triage_summary.json +24 -0
  19. package/.mcp/schemas/github_create_issue.json +58 -0
  20. package/.mcp/schemas/github_create_pull_request.json +56 -0
  21. package/.mcp/schemas/github_get_merge_conflict_files.json +24 -0
  22. package/.mcp/schemas/github_get_pr.json +32 -0
  23. package/.mcp/schemas/github_get_pr_diff.json +32 -0
  24. package/.mcp/schemas/github_issue_comment.json +38 -0
  25. package/.mcp/schemas/github_issue_update.json +123 -0
  26. package/.mcp/schemas/github_issue_view.json +32 -0
  27. package/.mcp/schemas/github_open_replacement_pr.json +67 -0
  28. package/.mcp/schemas/github_search_open_prs.json +65 -0
  29. package/.mcp/schemas/jules.cancel_session.json +16 -12
  30. package/.mcp/schemas/jules.create_session.json +41 -37
  31. package/.mcp/schemas/jules.get_messages.json +16 -12
  32. package/.mcp/schemas/jules.get_pr.json +16 -12
  33. package/.mcp/schemas/jules.get_session.json +16 -12
  34. package/.mcp/schemas/jules.list_sessions.json +33 -29
  35. package/.mcp/schemas/jules.send_message.json +30 -26
  36. package/.mcp/schemas/jules.trigger_feedback.json +16 -12
  37. package/.mcp/schemas/jules_cancel_session.json +18 -0
  38. package/.mcp/schemas/jules_create_session.json +44 -0
  39. package/.mcp/schemas/jules_get_messages.json +18 -0
  40. package/.mcp/schemas/jules_get_pr.json +18 -0
  41. package/.mcp/schemas/jules_get_session.json +18 -0
  42. package/.mcp/schemas/jules_list_sessions.json +36 -0
  43. package/.mcp/schemas/jules_send_message.json +35 -0
  44. package/.mcp/schemas/jules_trigger_feedback.json +18 -0
  45. package/.mcp/schemas/repo.commit_patch.json +34 -30
  46. package/.mcp/schemas/repo.create_branch.json +21 -17
  47. package/.mcp/schemas/repo.create_repair_branch.json +34 -30
  48. package/.mcp/schemas/repo.get_changed_files.json +19 -15
  49. package/.mcp/schemas/repo.get_command_schema.json +16 -12
  50. package/.mcp/schemas/repo.get_package_scripts.json +21 -17
  51. package/.mcp/schemas/repo.get_route_map.json +21 -17
  52. package/.mcp/schemas/repo.logs.json +28 -24
  53. package/.mcp/schemas/repo.read_agent_context.json +7 -3
  54. package/.mcp/schemas/repo.read_ci_logs.json +21 -17
  55. package/.mcp/schemas/repo.run_lighthouse.json +26 -22
  56. package/.mcp/schemas/repo.run_playwright.json +33 -29
  57. package/.mcp/schemas/repo.run_tests.json +40 -36
  58. package/.mcp/schemas/repo_commit_patch.json +39 -0
  59. package/.mcp/schemas/repo_create_branch.json +24 -0
  60. package/.mcp/schemas/repo_create_repair_branch.json +37 -0
  61. package/.mcp/schemas/repo_get_changed_files.json +22 -0
  62. package/.mcp/schemas/repo_get_command_schema.json +18 -0
  63. package/.mcp/schemas/repo_get_package_scripts.json +23 -0
  64. package/.mcp/schemas/repo_get_route_map.json +23 -0
  65. package/.mcp/schemas/repo_logs.json +31 -0
  66. package/.mcp/schemas/repo_read_agent_context.json +9 -0
  67. package/.mcp/schemas/repo_read_ci_logs.json +24 -0
  68. package/.mcp/schemas/repo_run_lighthouse.json +29 -0
  69. package/.mcp/schemas/repo_run_playwright.json +36 -0
  70. package/.mcp/schemas/repo_run_tests.json +45 -0
  71. package/CHANGELOG.md +16 -0
  72. package/dist/config.js +43 -17
  73. package/dist/config.test.js +18 -3
  74. package/dist/lib/git.js +30 -0
  75. package/dist/lib/param_utils.js +35 -0
  76. package/dist/lib/param_utils.test.js +44 -0
  77. package/dist/mcp/definitions.js +11 -1
  78. package/dist/mcp/schemas.test.js +28 -0
  79. package/dist/mcp/server.js +72 -36
  80. package/dist/tools/contract.js +94 -8
  81. package/dist/tools/github.create_issue.js +3 -2
  82. package/dist/tools/github.create_issue.test.js +10 -0
  83. package/dist/tools/github.create_pull_request.js +4 -4
  84. package/dist/tools/github.get_pr.js +4 -1
  85. package/dist/tools/github.get_pr_diff.js +6 -2
  86. package/dist/tools/github.issue_comment.js +8 -1
  87. package/dist/tools/github.issue_update.js +3 -0
  88. package/dist/tools/github.issue_view.js +4 -1
  89. package/dist/tools/github.search_open_prs.js +4 -0
  90. package/dist/tools/repo.commit_patch.js +30 -10
  91. package/dist/tools/repo.run_playwright.js +10 -5
  92. package/dist/tools/repo.run_tests.js +26 -4
  93. package/package.json +2 -2
  94. package/scripts/sync-mcp-schemas.ts +6 -1
  95. package/src/config.test.ts +22 -3
  96. package/src/config.ts +47 -17
  97. package/src/lib/git.ts +34 -0
  98. package/src/lib/param_utils.test.ts +49 -0
  99. package/src/lib/param_utils.ts +42 -0
  100. package/src/mcp/definitions.ts +12 -1
  101. package/src/mcp/schemas.test.ts +34 -0
  102. package/src/mcp/server.ts +73 -38
  103. package/src/tools/contract.ts +94 -8
  104. package/src/tools/github.create_issue.test.ts +10 -0
  105. package/src/tools/github.create_issue.ts +3 -1
  106. package/src/tools/github.create_pull_request.ts +4 -5
  107. package/src/tools/github.get_pr.ts +4 -1
  108. package/src/tools/github.get_pr_diff.ts +7 -2
  109. package/src/tools/github.issue_comment.ts +8 -1
  110. package/src/tools/github.issue_update.ts +4 -1
  111. package/src/tools/github.issue_view.ts +4 -1
  112. package/src/tools/github.search_open_prs.ts +4 -0
  113. package/src/tools/repo.commit_patch.ts +35 -11
  114. package/src/tools/repo.run_playwright.ts +13 -6
  115. package/src/tools/repo.run_tests.ts +28 -5
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "repo_run_lighthouse",
3
+ "description": "Run Lighthouse CI audits.",
4
+ "parameters": {
5
+ "properties": {
6
+ "route": {
7
+ "default": "/",
8
+ "description": "The route to audit (default: '/').",
9
+ "title": "Route",
10
+ "type": "string"
11
+ },
12
+ "worktreePath": {
13
+ "anyOf": [
14
+ {
15
+ "type": "string"
16
+ },
17
+ {
18
+ "type": "null"
19
+ }
20
+ ],
21
+ "default": null,
22
+ "description": "Optional path to the worktree to run the audit in.",
23
+ "title": "Worktreepath"
24
+ }
25
+ },
26
+ "title": "RunLighthouseInput",
27
+ "type": "object"
28
+ }
29
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "repo_run_playwright",
3
+ "description": "Run Playwright E2E tests.",
4
+ "parameters": {
5
+ "properties": {
6
+ "grep": {
7
+ "anyOf": [
8
+ {
9
+ "type": "string"
10
+ },
11
+ {
12
+ "type": "null"
13
+ }
14
+ ],
15
+ "default": null,
16
+ "description": "Optional pattern to filter tests by name.",
17
+ "title": "Grep"
18
+ },
19
+ "worktreePath": {
20
+ "anyOf": [
21
+ {
22
+ "type": "string"
23
+ },
24
+ {
25
+ "type": "null"
26
+ }
27
+ ],
28
+ "default": null,
29
+ "description": "Optional path to the worktree to run tests in.",
30
+ "title": "Worktreepath"
31
+ }
32
+ },
33
+ "title": "RunPlaywrightInput",
34
+ "type": "object"
35
+ }
36
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "repo_run_tests",
3
+ "description": "Run repository tests and checks.",
4
+ "parameters": {
5
+ "properties": {
6
+ "commands": {
7
+ "anyOf": [
8
+ {
9
+ "items": {
10
+ "type": "string"
11
+ },
12
+ "type": "array"
13
+ },
14
+ {
15
+ "type": "null"
16
+ }
17
+ ],
18
+ "default": null,
19
+ "description": "Optional list of commands to run (default includes install, lint, test, build).",
20
+ "title": "Commands"
21
+ },
22
+ "timeoutSeconds": {
23
+ "default": 300,
24
+ "description": "Maximum time in seconds to wait for tests (default: 300).",
25
+ "title": "Timeoutseconds",
26
+ "type": "integer"
27
+ },
28
+ "worktreePath": {
29
+ "anyOf": [
30
+ {
31
+ "type": "string"
32
+ },
33
+ {
34
+ "type": "null"
35
+ }
36
+ ],
37
+ "default": null,
38
+ "description": "Optional path to the worktree to run tests in.",
39
+ "title": "Worktreepath"
40
+ }
41
+ },
42
+ "title": "RunTestsInput",
43
+ "type": "object"
44
+ }
45
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2](https://github.com/arii/boomtick/compare/mcp-v0.4.1...mcp-v0.4.2) (2026-10-12)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **mcp:** map tool JSON Schema to parameters object in sync-mcp-schemas ([#802](https://github.com/arii/boomtick/issues/802)) ([a57a9ae](https://github.com/arii/boomtick/commit/a57a9aef32109242882d682fcbd1a9ff0a81cffe))
9
+ * **mcp:** resolve identifier regex, commit patch, timeout, and parameter casing bugs ([#815](https://github.com/arii/boomtick/issues/815)) ([1f3b232](https://github.com/arii/boomtick/commit/1f3b232d8550fa87eac2dd974bfd67252a8f4da1))
10
+ * **mcp:** support dynamic repo context resolution and --repo forwarding across workspaces ([#811](https://github.com/arii/boomtick/issues/811)) ([f7e1665](https://github.com/arii/boomtick/commit/f7e166594f170843b7f2e267fab119c631bf3c83))
11
+
12
+ ## [0.4.1](https://github.com/arii/boomtick/compare/mcp-v0.4.0...mcp-v0.4.1) (2026-09-22)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **cli,mcp:** improve workspace config resolution and github repo detection ([#787](https://github.com/arii/boomtick/issues/787)) ([0103e90](https://github.com/arii/boomtick/commit/0103e904d30213cc336a1ce9567ad59257494236))
18
+
3
19
  ## [0.4.0](https://github.com/arii/boomtick/compare/mcp-v0.3.7...mcp-v0.4.0) (2026-09-04)
4
20
 
5
21
 
package/dist/config.js CHANGED
@@ -1,13 +1,17 @@
1
- import dotenv from "dotenv";
2
1
  import path from "path";
3
2
  import fs from "fs";
4
3
  import { fileURLToPath } from "url";
5
4
  import { execSync } from "child_process";
6
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- dotenv.config({
8
- path: path.resolve(__dirname, "../.env"),
9
- quiet: true
10
- });
6
+ const envPath = path.resolve(__dirname, "../.env");
7
+ if (fs.existsSync(envPath) && typeof process.loadEnvFile === "function") {
8
+ try {
9
+ process.loadEnvFile(envPath);
10
+ }
11
+ catch {
12
+ // Ignore if already loaded or invalid
13
+ }
14
+ }
11
15
  function getGithubToken() {
12
16
  if (process.env.GITHUB_TOKEN) {
13
17
  return process.env.GITHUB_TOKEN;
@@ -31,9 +35,11 @@ export function initializeConfig() {
31
35
  return cachedDynamicConfig;
32
36
  }
33
37
  try {
34
- // Attempt to load core properties from the Python CLI to avoid duplication
38
+ // Attempt to load core properties from the Python CLI in the active workspace
39
+ const cwd = process.env.BOOMTICK_REPO_PATH || process.cwd();
35
40
  const cmd = `td-cli config view`;
36
41
  const output = execSync(cmd, {
42
+ cwd,
37
43
  encoding: "utf-8",
38
44
  stdio: ["ignore", "pipe", "ignore"]
39
45
  });
@@ -58,10 +64,22 @@ function findRepoRoot() {
58
64
  if (process.env.BOOMTICK_REPO_PATH) {
59
65
  return process.env.BOOMTICK_REPO_PATH;
60
66
  }
61
- // Traverse up to find workspace.json or .git
62
- let current = __dirname;
67
+ // Traverse up from process.cwd() to find project_config.json, workspace.json, or .git
68
+ let current = process.cwd();
63
69
  while (current !== path.parse(current).root) {
64
- if (fs.existsSync(path.join(current, "workspace.json")) || fs.existsSync(path.join(current, ".git"))) {
70
+ if (fs.existsSync(path.join(current, "project_config.json")) ||
71
+ fs.existsSync(path.join(current, "workspace.json")) ||
72
+ fs.existsSync(path.join(current, ".git"))) {
73
+ return current;
74
+ }
75
+ current = path.dirname(current);
76
+ }
77
+ // Fallback to __dirname search if discovery from cwd fails
78
+ current = __dirname;
79
+ while (current !== path.parse(current).root) {
80
+ if (fs.existsSync(path.join(current, "project_config.json")) ||
81
+ fs.existsSync(path.join(current, "workspace.json")) ||
82
+ fs.existsSync(path.join(current, ".git"))) {
65
83
  return current;
66
84
  }
67
85
  current = path.dirname(current);
@@ -72,22 +90,30 @@ function findRepoRoot() {
72
90
  export const config = {
73
91
  get githubToken() { return getGithubToken(); },
74
92
  get githubOwner() {
93
+ const repoString = cachedDynamicConfig?.github_repo;
94
+ if (typeof repoString === "string" && repoString.includes("/")) {
95
+ return repoString.split("/")[0];
96
+ }
75
97
  if (process.env.GITHUB_OWNER)
76
98
  return process.env.GITHUB_OWNER;
77
- const repoString = cachedDynamicConfig?.github_repo;
78
- if (typeof repoString !== "string" || !repoString.includes("/")) {
79
- throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
99
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
100
+ if (envRepo && envRepo.includes("/")) {
101
+ return envRepo.split("/")[0];
80
102
  }
81
- return repoString.split("/")[0];
103
+ throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
82
104
  },
83
105
  get githubRepo() {
106
+ const repoString = cachedDynamicConfig?.github_repo;
107
+ if (typeof repoString === "string" && repoString.includes("/")) {
108
+ return repoString.split("/")[1];
109
+ }
84
110
  if (process.env.GITHUB_REPO)
85
111
  return process.env.GITHUB_REPO;
86
- const repoString = cachedDynamicConfig?.github_repo;
87
- if (typeof repoString !== "string" || !repoString.includes("/")) {
88
- throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
112
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
113
+ if (envRepo && envRepo.includes("/")) {
114
+ return envRepo.split("/")[1];
89
115
  }
90
- return repoString.split("/")[1];
116
+ throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
91
117
  },
92
118
  get repoPath() {
93
119
  return findRepoRoot();
@@ -15,12 +15,14 @@ describe("config", () => {
15
15
  process.env = originalEnv;
16
16
  });
17
17
  describe("initializeConfig", () => {
18
- it("should load dynamic config from td-cli config view", async () => {
18
+ it("should load dynamic config from td-cli config view using active cwd", async () => {
19
19
  const mockConfig = { github_repo: "owner/repo", base_branch: "origin/main", vite_base_path: "/app/" };
20
20
  vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
21
21
  const { initializeConfig } = await import("./config.js");
22
22
  const result = initializeConfig();
23
- expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.anything());
23
+ expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.objectContaining({
24
+ cwd: process.cwd()
25
+ }));
24
26
  expect(result).toEqual(mockConfig);
25
27
  });
26
28
  it("should return cached dynamic config on subsequent calls", async () => {
@@ -67,7 +69,18 @@ describe("config", () => {
67
69
  });
68
70
  expect(config.githubToken).toBeUndefined();
69
71
  });
70
- it("should resolve githubOwner and githubRepo from env or cached config", async () => {
72
+ it("should prioritize cachedDynamicConfig over environment variables for githubOwner and githubRepo", async () => {
73
+ process.env.GITHUB_OWNER = "env-owner";
74
+ process.env.GITHUB_REPO = "env-repo";
75
+ process.env.GITHUB_REPOSITORY = "env-owner/env-repo";
76
+ const mockConfig = { github_repo: "workspace-owner/workspace-repo" };
77
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
78
+ const { config, initializeConfig } = await import("./config.js");
79
+ initializeConfig();
80
+ expect(config.githubOwner).toBe("workspace-owner");
81
+ expect(config.githubRepo).toBe("workspace-repo");
82
+ });
83
+ it("should fallback to environment variables when cachedDynamicConfig is empty", async () => {
71
84
  process.env.GITHUB_OWNER = "custom-owner";
72
85
  process.env.GITHUB_REPO = "custom-repo";
73
86
  const { config } = await import("./config.js");
@@ -77,6 +90,8 @@ describe("config", () => {
77
90
  it("should throw when githubOwner / githubRepo cannot be determined", async () => {
78
91
  delete process.env.GITHUB_OWNER;
79
92
  delete process.env.GITHUB_REPO;
93
+ delete process.env.GITHUB_REPOSITORY;
94
+ delete process.env.GH_REPO;
80
95
  vi.mocked(execSync).mockImplementation(() => {
81
96
  throw new Error("td-cli failed");
82
97
  });
package/dist/lib/git.js CHANGED
@@ -1,6 +1,36 @@
1
1
  import { runCommand } from "./shell.js";
2
+ import { config } from "../config.js";
2
3
  import path from "path";
3
4
  import fs from "fs/promises";
5
+ import { execSync } from "child_process";
6
+ const repoCache = new Map();
7
+ export function resolveRepo(explicitRepo, cwd) {
8
+ if (explicitRepo && explicitRepo.trim().length > 0) {
9
+ return explicitRepo.trim();
10
+ }
11
+ const targetCwd = cwd || process.cwd();
12
+ if (repoCache.has(targetCwd)) {
13
+ return repoCache.get(targetCwd);
14
+ }
15
+ try {
16
+ const url = execSync("git remote get-url origin", {
17
+ cwd: targetCwd,
18
+ encoding: "utf-8",
19
+ stdio: ["ignore", "pipe", "ignore"],
20
+ }).trim();
21
+ const match = url.match(/[:/]([^/]+\/[^/.]+)(\.git)?$/);
22
+ if (match) {
23
+ repoCache.set(targetCwd, match[1]);
24
+ return match[1];
25
+ }
26
+ }
27
+ catch (e) {
28
+ // Ignore error and fall back to static config
29
+ }
30
+ const fallback = `${config.githubOwner}/${config.githubRepo}`;
31
+ repoCache.set(targetCwd, fallback);
32
+ return fallback;
33
+ }
4
34
  function validatePrNumber(value) {
5
35
  const prNumber = Number(value);
6
36
  if (!Number.isInteger(prNumber) || prNumber <= 0) {
@@ -0,0 +1,35 @@
1
+ export function normalizeArguments(args) {
2
+ if (!args || typeof args !== "object") {
3
+ return {};
4
+ }
5
+ const result = {};
6
+ for (const [key, value] of Object.entries(args)) {
7
+ // Convert snake_case to camelCase
8
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
9
+ // Normalize value recursively if it's an object/array
10
+ let normalizedValue = value;
11
+ if (Array.isArray(value)) {
12
+ normalizedValue = value.map((item) => item && typeof item === "object" ? normalizeArguments(item) : item);
13
+ }
14
+ else if (value && typeof value === "object" && !(value instanceof RegExp)) {
15
+ normalizedValue = normalizeArguments(value);
16
+ }
17
+ // Coerce numeric string to integer for prNumber / issueNumber if applicable
18
+ if ((camelKey === "prNumber" || camelKey === "issueNumber") && typeof normalizedValue === "string") {
19
+ const parsedNum = parseInt(normalizedValue, 10);
20
+ if (!isNaN(parsedNum)) {
21
+ normalizedValue = parsedNum;
22
+ }
23
+ }
24
+ result[camelKey] = normalizedValue;
25
+ }
26
+ // Parameter aliases harmonization
27
+ // branch vs branchName harmonization
28
+ if (result.branch !== undefined && result.branchName === undefined) {
29
+ result.branchName = result.branch;
30
+ }
31
+ else if (result.branchName !== undefined && result.branch === undefined) {
32
+ result.branch = result.branchName;
33
+ }
34
+ return result;
35
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { normalizeArguments } from "./param_utils.js";
3
+ describe("param_utils.normalizeArguments", () => {
4
+ it("should convert snake_case keys to camelCase", () => {
5
+ const input = {
6
+ pr_number: 123,
7
+ issue_number: 456,
8
+ worktree_path: "/tmp/worktree",
9
+ base_branch: "main",
10
+ };
11
+ const output = normalizeArguments(input);
12
+ expect(output).toEqual({
13
+ prNumber: 123,
14
+ issueNumber: 456,
15
+ worktreePath: "/tmp/worktree",
16
+ baseBranch: "main",
17
+ });
18
+ });
19
+ it("should coerce numeric strings for prNumber and issueNumber", () => {
20
+ const input = {
21
+ pr_number: "789",
22
+ issue_number: "101",
23
+ };
24
+ const output = normalizeArguments(input);
25
+ expect(output).toEqual({
26
+ prNumber: 789,
27
+ issueNumber: 101,
28
+ });
29
+ });
30
+ it("should synchronize branch and branchName", () => {
31
+ expect(normalizeArguments({ branch: "feat-x" })).toEqual({
32
+ branch: "feat-x",
33
+ branchName: "feat-x",
34
+ });
35
+ expect(normalizeArguments({ branch_name: "feat-y" })).toEqual({
36
+ branchName: "feat-y",
37
+ branch: "feat-y",
38
+ });
39
+ });
40
+ it("should handle null or empty arguments safely", () => {
41
+ expect(normalizeArguments(null)).toEqual({});
42
+ expect(normalizeArguments(undefined)).toEqual({});
43
+ });
44
+ });
@@ -63,7 +63,7 @@ export const MCP_RESOURCES = [
63
63
  description: "Playwright test report for a specific branch.",
64
64
  },
65
65
  ];
66
- export const MCP_TOOLS = [
66
+ const BASE_MCP_TOOLS = [
67
67
  {
68
68
  name: "boomtick.health",
69
69
  description: "Check the health and configuration of the MCP server.",
@@ -240,3 +240,13 @@ export const MCP_TOOLS = [
240
240
  inputSchema: SearchDdgsInputJsonSchema,
241
241
  },
242
242
  ];
243
+ export const MCP_TOOLS = BASE_MCP_TOOLS.flatMap((tool) => {
244
+ const underscoreName = tool.name.replace(/\./g, "_");
245
+ if (underscoreName !== tool.name) {
246
+ return [
247
+ { ...tool, name: underscoreName },
248
+ tool,
249
+ ];
250
+ }
251
+ return [tool];
252
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { execSync } from "child_process";
5
+ describe("MCP tool schemas export", () => {
6
+ it("should generate valid schema files with non-null parameters object", () => {
7
+ const schemasDir = path.resolve(process.cwd(), ".mcp/schemas");
8
+ if (!fs.existsSync(schemasDir)) {
9
+ execSync("npx tsx scripts/sync-mcp-schemas.ts", { cwd: process.cwd() });
10
+ }
11
+ expect(fs.existsSync(schemasDir)).toBe(true);
12
+ const files = fs.readdirSync(schemasDir).filter((f) => f.endsWith(".json"));
13
+ expect(files.length).toBeGreaterThan(0);
14
+ for (const file of files) {
15
+ const filePath = path.join(schemasDir, file);
16
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
17
+ expect(content).toHaveProperty("name");
18
+ expect(typeof content.name).toBe("string");
19
+ expect(content).toHaveProperty("description");
20
+ expect(typeof content.description).toBe("string");
21
+ expect(content).toHaveProperty("parameters");
22
+ expect(content.parameters).not.toBeNull();
23
+ expect(typeof content.parameters).toBe("object");
24
+ expect(content.parameters.type).toBe("object");
25
+ expect(content.parameters.properties).toBeDefined();
26
+ }
27
+ });
28
+ });
@@ -4,6 +4,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
4
4
  import { config } from "../config.js";
5
5
  import { createSuccessResult, createErrorResult } from "../lib/result.js";
6
6
  import { MCP_TOOLS, MCP_PROMPTS, MCP_RESOURCES } from "./definitions.js";
7
+ import { normalizeArguments } from "../lib/param_utils.js";
7
8
  import { healthHandler, HealthCheckInputSchema } from "./tools.js";
8
9
  import { searchOpenPrsHandler, SearchOpenPrsInputSchema } from "../tools/github.search_open_prs.js";
9
10
  import { getPrDiffHandler, GetPrDiffInputSchema } from "../tools/github.get_pr_diff.js";
@@ -157,77 +158,112 @@ export class BoomtickMCPServer {
157
158
  });
158
159
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
159
160
  try {
160
- switch (request.params.name) {
161
+ const normalizedToolName = request.params.name.replace(/_/g, ".");
162
+ const normArgs = normalizeArguments(request.params.arguments);
163
+ switch (normalizedToolName) {
161
164
  case "boomtick.health":
162
- return createSuccessResult(await healthHandler(HealthCheckInputSchema.parse(request.params.arguments || {})));
165
+ return createSuccessResult(await healthHandler(HealthCheckInputSchema.parse(normArgs)));
166
+ case "github.search.open.prs":
163
167
  case "github.search_open_prs":
164
- return createSuccessResult(await searchOpenPrsHandler(SearchOpenPrsInputSchema.parse(request.params.arguments || {})));
168
+ return createSuccessResult(await searchOpenPrsHandler(SearchOpenPrsInputSchema.parse(normArgs)));
169
+ case "repo.read.agent.context":
165
170
  case "repo.read_agent_context":
166
- return createSuccessResult(await readAgentContextHandler(ReadAgentContextInputSchema.parse(request.params.arguments || {})));
171
+ return createSuccessResult(await readAgentContextHandler(ReadAgentContextInputSchema.parse(normArgs)));
172
+ case "github.get.pr.diff":
167
173
  case "github.get_pr_diff":
168
- return createSuccessResult(await getPrDiffHandler(GetPrDiffInputSchema.parse(request.params.arguments)));
174
+ return createSuccessResult(await getPrDiffHandler(GetPrDiffInputSchema.parse(normArgs)));
175
+ case "github.get.merge.conflict.files":
169
176
  case "github.get_merge_conflict_files":
170
- return createSuccessResult(await getMergeConflictFilesHandler(GetMergeConflictFilesInputSchema.parse(request.params.arguments)));
177
+ return createSuccessResult(await getMergeConflictFilesHandler(GetMergeConflictFilesInputSchema.parse(normArgs)));
178
+ case "github.checkout.branch":
171
179
  case "github.checkout_branch":
172
- return createSuccessResult(await checkoutBranchHandler(CheckoutBranchInputSchema.parse(request.params.arguments)));
180
+ return createSuccessResult(await checkoutBranchHandler(CheckoutBranchInputSchema.parse(normArgs)));
181
+ case "repo.get.changed.files":
173
182
  case "repo.get_changed_files":
174
- return createSuccessResult(await getChangedFilesHandler(GetChangedFilesInputSchema.parse(request.params.arguments || {})));
183
+ return createSuccessResult(await getChangedFilesHandler(GetChangedFilesInputSchema.parse(normArgs)));
184
+ case "repo.get.command.schema":
175
185
  case "repo.get_command_schema":
176
- return createSuccessResult(await getCommandSchemaHandler(GetCommandSchemaInputSchema.parse(request.params.arguments)));
186
+ return createSuccessResult(await getCommandSchemaHandler(GetCommandSchemaInputSchema.parse(normArgs)));
187
+ case "repo.get.package.scripts":
177
188
  case "repo.get_package_scripts":
178
- return createSuccessResult(await getPackageScriptsHandler(GetPackageScriptsInputSchema.parse(request.params.arguments || {})));
189
+ return createSuccessResult(await getPackageScriptsHandler(GetPackageScriptsInputSchema.parse(normArgs)));
190
+ case "repo.get.route.map":
179
191
  case "repo.get_route_map":
180
- return createSuccessResult(await getRouteMapHandler(GetRouteMapInputSchema.parse(request.params.arguments || {})));
192
+ return createSuccessResult(await getRouteMapHandler(GetRouteMapInputSchema.parse(normArgs)));
193
+ case "repo.read.ci.logs":
181
194
  case "repo.read_ci_logs":
182
- return createSuccessResult(await readCiLogsHandler(ReadCiLogsInputSchema.parse(request.params.arguments)));
195
+ return createSuccessResult(await readCiLogsHandler(ReadCiLogsInputSchema.parse(normArgs)));
183
196
  case "repo.logs":
184
- return createSuccessResult(await repoLogsHandler(RepoLogsInputSchema.parse(request.params.arguments)));
197
+ return createSuccessResult(await repoLogsHandler(RepoLogsInputSchema.parse(normArgs)));
198
+ case "repo.create.branch":
185
199
  case "repo.create_branch":
186
- return createSuccessResult(await createBranchHandler(CreateBranchInputSchema.parse(request.params.arguments)));
200
+ return createSuccessResult(await createBranchHandler(CreateBranchInputSchema.parse(normArgs)));
201
+ case "repo.create.repair.branch":
187
202
  case "repo.create_repair_branch":
188
- return createSuccessResult(await createRepairBranchHandler(CreateRepairBranchInputSchema.parse(request.params.arguments)));
203
+ return createSuccessResult(await createRepairBranchHandler(CreateRepairBranchInputSchema.parse(normArgs)));
204
+ case "repo.run.tests":
189
205
  case "repo.run_tests":
190
- return createSuccessResult(await runTestsHandler(RunTestsInputSchema.parse(request.params.arguments || {})));
206
+ return createSuccessResult(await runTestsHandler(RunTestsInputSchema.parse(normArgs)));
207
+ case "repo.run.lighthouse":
191
208
  case "repo.run_lighthouse":
192
- return createSuccessResult(await runLighthouseHandler(RunLighthouseInputSchema.parse(request.params.arguments || {})));
209
+ return createSuccessResult(await runLighthouseHandler(RunLighthouseInputSchema.parse(normArgs)));
210
+ case "repo.run.playwright":
193
211
  case "repo.run_playwright":
194
- return createSuccessResult(await runPlaywrightHandler(RunPlaywrightInputSchema.parse(request.params.arguments || {})));
212
+ return createSuccessResult(await runPlaywrightHandler(RunPlaywrightInputSchema.parse(normArgs)));
213
+ case "repo.commit.patch":
195
214
  case "repo.commit_patch":
196
- return createSuccessResult(await commitPatchHandler(CommitPatchInputSchema.parse(request.params.arguments)));
215
+ return createSuccessResult(await commitPatchHandler(CommitPatchInputSchema.parse(normArgs)));
216
+ case "github.open.replacement.pr":
197
217
  case "github.open_replacement_pr":
198
- return createSuccessResult(await openReplacementPrHandler(OpenReplacementPrInputSchema.parse(request.params.arguments)));
218
+ return createSuccessResult(await openReplacementPrHandler(OpenReplacementPrInputSchema.parse(normArgs)));
219
+ case "github.comment.triage.summary":
199
220
  case "github.comment_triage_summary":
200
- return createSuccessResult(await commentTriageSummaryHandler(CommentTriageSummaryInputSchema.parse(request.params.arguments)));
221
+ return createSuccessResult(await commentTriageSummaryHandler(CommentTriageSummaryInputSchema.parse(normArgs)));
222
+ case "github.create.pull.request":
201
223
  case "github.create_pull_request":
202
- return createSuccessResult(await createPullRequestHandler(CreatePullRequestInputSchema.parse(request.params.arguments)));
224
+ return createSuccessResult(await createPullRequestHandler(CreatePullRequestInputSchema.parse(normArgs)));
225
+ case "github.get.pr":
203
226
  case "github.get_pr":
204
- return createSuccessResult(await getPrHandler(GetPrInputSchema.parse(request.params.arguments)));
227
+ return createSuccessResult(await getPrHandler(GetPrInputSchema.parse(normArgs)));
228
+ case "github.issue.view":
205
229
  case "github.issue_view":
206
- return createSuccessResult(await issueViewHandler(IssueViewInputSchema.parse(request.params.arguments)));
230
+ return createSuccessResult(await issueViewHandler(IssueViewInputSchema.parse(normArgs)));
231
+ case "github.issue.update":
207
232
  case "github.issue_update":
208
- return createSuccessResult(await issueUpdateHandler(IssueUpdateInputSchema.parse(request.params.arguments)));
233
+ return createSuccessResult(await issueUpdateHandler(IssueUpdateInputSchema.parse(normArgs)));
234
+ case "github.issue.comment":
209
235
  case "github.issue_comment":
210
- return createSuccessResult(await issueCommentHandler(IssueCommentInputSchema.parse(request.params.arguments)));
236
+ return createSuccessResult(await issueCommentHandler(IssueCommentInputSchema.parse(normArgs)));
237
+ case "github.create.issue":
211
238
  case "github.create_issue":
212
- return createSuccessResult(await createIssueHandler(CreateIssueInputSchema.parse(request.params.arguments)));
239
+ return createSuccessResult(await createIssueHandler(CreateIssueInputSchema.parse(normArgs)));
240
+ case "jules.create.session":
213
241
  case "jules.create_session":
214
- return createSuccessResult(await createJulesSessionHandler(CreateJulesSessionInputSchema.parse(request.params.arguments)));
242
+ return createSuccessResult(await createJulesSessionHandler(CreateJulesSessionInputSchema.parse(normArgs)));
243
+ case "jules.get.session":
215
244
  case "jules.get_session":
216
- return createSuccessResult(await getJulesSessionHandler(GetJulesSessionInputSchema.parse(request.params.arguments)));
245
+ return createSuccessResult(await getJulesSessionHandler(GetJulesSessionInputSchema.parse(normArgs)));
246
+ case "jules.send.message":
217
247
  case "jules.send_message":
218
- return createSuccessResult(await sendJulesMessageHandler(SendJulesMessageInputSchema.parse(request.params.arguments)));
248
+ return createSuccessResult(await sendJulesMessageHandler(SendJulesMessageInputSchema.parse(normArgs)));
249
+ case "jules.get.messages":
219
250
  case "jules.get_messages":
220
- return createSuccessResult(await getJulesMessagesHandler(GetJulesMessagesInputSchema.parse(request.params.arguments)));
251
+ return createSuccessResult(await getJulesMessagesHandler(GetJulesMessagesInputSchema.parse(normArgs)));
252
+ case "jules.list.sessions":
221
253
  case "jules.list_sessions":
222
- return createSuccessResult(await listJulesSessionsHandler(ListJulesSessionsInputSchema.parse(request.params.arguments || {})));
254
+ return createSuccessResult(await listJulesSessionsHandler(ListJulesSessionsInputSchema.parse(normArgs)));
255
+ case "jules.cancel.session":
223
256
  case "jules.cancel_session":
224
- return createSuccessResult(await cancelJulesSessionHandler(CancelJulesSessionInputSchema.parse(request.params.arguments)));
257
+ return createSuccessResult(await cancelJulesSessionHandler(CancelJulesSessionInputSchema.parse(normArgs)));
258
+ case "jules.get.pr":
225
259
  case "jules.get_pr":
226
- return createSuccessResult(await getJulesPullRequestHandler(GetJulesPullRequestInputSchema.parse(request.params.arguments)));
260
+ return createSuccessResult(await getJulesPullRequestHandler(GetJulesPullRequestInputSchema.parse(normArgs)));
261
+ case "jules.trigger.feedback":
227
262
  case "jules.trigger_feedback":
228
- return createSuccessResult(await triggerJulesFeedbackHandler(TriggerJulesFeedbackInputSchema.parse(request.params.arguments)));
263
+ return createSuccessResult(await triggerJulesFeedbackHandler(TriggerJulesFeedbackInputSchema.parse(normArgs)));
264
+ case "agent.search.ddgs":
229
265
  case "agent.search_ddgs":
230
- return createSuccessResult(await ddgsSearchHandler(DdgsSearchInputSchema.parse(request.params.arguments)));
266
+ return createSuccessResult(await ddgsSearchHandler(DdgsSearchInputSchema.parse(normArgs)));
231
267
  default:
232
268
  return createErrorResult(`Tool not found: ${request.params.name}`);
233
269
  }