@arii/boomtick-mcp 0.3.7 → 0.4.1

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.
@@ -30,6 +30,19 @@
30
30
  ],
31
31
  "default": null,
32
32
  "title": "File"
33
+ },
34
+ "repo": {
35
+ "anyOf": [
36
+ {
37
+ "type": "string"
38
+ },
39
+ {
40
+ "type": "null"
41
+ }
42
+ ],
43
+ "default": null,
44
+ "description": "The target repository override (e.g. org/repo).",
45
+ "title": "Repo"
33
46
  }
34
47
  },
35
48
  "required": [
@@ -26,6 +26,19 @@
26
26
  "description": "Whether to create the PR as a draft.",
27
27
  "title": "Draft",
28
28
  "type": "boolean"
29
+ },
30
+ "repo": {
31
+ "anyOf": [
32
+ {
33
+ "type": "string"
34
+ },
35
+ {
36
+ "type": "null"
37
+ }
38
+ ],
39
+ "default": null,
40
+ "description": "The target repository override (e.g. org/repo).",
41
+ "title": "Repo"
29
42
  }
30
43
  },
31
44
  "required": [
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1](https://github.com/arii/boomtick/compare/mcp-v0.4.0...mcp-v0.4.1) (2026-09-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **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))
9
+
10
+ ## [0.4.0](https://github.com/arii/boomtick/compare/mcp-v0.3.7...mcp-v0.4.0) (2026-09-04)
11
+
12
+
13
+ ### Features
14
+
15
+ * **impact:** add targeted route and viewport options for visual diff… ([#775](https://github.com/arii/boomtick/issues/775)) ([c767fd4](https://github.com/arii/boomtick/commit/c767fd47063ce6a1078a728c3af79bcf18e2c5c5))
16
+
3
17
  ## [0.3.7](https://github.com/arii/boomtick/compare/mcp-v0.3.6...mcp-v0.3.7) (2026-08-17)
4
18
 
5
19
 
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();
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { execSync } from "child_process";
3
+ vi.mock("child_process", () => ({
4
+ execSync: vi.fn(),
5
+ }));
6
+ describe("config", () => {
7
+ const originalEnv = process.env;
8
+ beforeEach(() => {
9
+ vi.resetModules();
10
+ process.env = { ...originalEnv };
11
+ vi.mocked(execSync).mockReset();
12
+ vi.mocked(execSync).mockReturnValue("{}");
13
+ });
14
+ afterEach(() => {
15
+ process.env = originalEnv;
16
+ });
17
+ describe("initializeConfig", () => {
18
+ it("should load dynamic config from td-cli config view using active cwd", async () => {
19
+ const mockConfig = { github_repo: "owner/repo", base_branch: "origin/main", vite_base_path: "/app/" };
20
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
21
+ const { initializeConfig } = await import("./config.js");
22
+ const result = initializeConfig();
23
+ expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.objectContaining({
24
+ cwd: process.cwd()
25
+ }));
26
+ expect(result).toEqual(mockConfig);
27
+ });
28
+ it("should return cached dynamic config on subsequent calls", async () => {
29
+ const mockConfig = { github_repo: "owner/repo" };
30
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
31
+ const { initializeConfig } = await import("./config.js");
32
+ const result1 = initializeConfig();
33
+ const result2 = initializeConfig();
34
+ expect(result1).toBe(result2);
35
+ expect(execSync).toHaveBeenCalledTimes(1);
36
+ });
37
+ it("should throw critical error when execSync fails in development or CI", async () => {
38
+ process.env.NODE_ENV = "development";
39
+ vi.mocked(execSync).mockImplementation(() => {
40
+ throw new Error("Command failed");
41
+ });
42
+ const { initializeConfig } = await import("./config.js");
43
+ expect(() => initializeConfig()).toThrow(/CRITICAL: Failed to load dynamic config/);
44
+ });
45
+ it("should log warning and set empty config when execSync fails in production", async () => {
46
+ process.env.NODE_ENV = "production";
47
+ process.env.CI = "false";
48
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
49
+ vi.mocked(execSync).mockImplementation(() => {
50
+ throw new Error("Command failed");
51
+ });
52
+ const { initializeConfig } = await import("./config.js");
53
+ const result = initializeConfig();
54
+ expect(result).toEqual({});
55
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("CRITICAL: Failed to load dynamic config"));
56
+ consoleWarnSpy.mockRestore();
57
+ });
58
+ });
59
+ describe("config object getters", () => {
60
+ it("should resolve githubToken from process.env or gh CLI fallback", async () => {
61
+ process.env.GITHUB_TOKEN = "env-token-123";
62
+ const { config } = await import("./config.js");
63
+ expect(config.githubToken).toBe("env-token-123");
64
+ delete process.env.GITHUB_TOKEN;
65
+ vi.mocked(execSync).mockReturnValue("gh-cli-token\n");
66
+ expect(config.githubToken).toBe("gh-cli-token");
67
+ vi.mocked(execSync).mockImplementation(() => {
68
+ throw new Error("gh not found");
69
+ });
70
+ expect(config.githubToken).toBeUndefined();
71
+ });
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 () => {
84
+ process.env.GITHUB_OWNER = "custom-owner";
85
+ process.env.GITHUB_REPO = "custom-repo";
86
+ const { config } = await import("./config.js");
87
+ expect(config.githubOwner).toBe("custom-owner");
88
+ expect(config.githubRepo).toBe("custom-repo");
89
+ });
90
+ it("should throw when githubOwner / githubRepo cannot be determined", async () => {
91
+ delete process.env.GITHUB_OWNER;
92
+ delete process.env.GITHUB_REPO;
93
+ delete process.env.GITHUB_REPOSITORY;
94
+ delete process.env.GH_REPO;
95
+ vi.mocked(execSync).mockImplementation(() => {
96
+ throw new Error("td-cli failed");
97
+ });
98
+ process.env.NODE_ENV = "production";
99
+ process.env.CI = "false";
100
+ vi.spyOn(console, "warn").mockImplementation(() => { });
101
+ const { config, initializeConfig } = await import("./config.js");
102
+ initializeConfig();
103
+ expect(() => config.githubOwner).toThrow("GITHUB_OWNER must be set via environment variable or project_config.json");
104
+ expect(() => config.githubRepo).toThrow("GITHUB_REPO must be set via environment variable or project_config.json");
105
+ });
106
+ it("should resolve repoPath from BOOMTICK_REPO_PATH or findRepoRoot fallback", async () => {
107
+ process.env.BOOMTICK_REPO_PATH = "/custom/repo/path";
108
+ const { config } = await import("./config.js");
109
+ expect(config.repoPath).toBe("/custom/repo/path");
110
+ });
111
+ it("should resolve defaultBaseBranch, viteBasePath, and ghPath", async () => {
112
+ process.env.DEFAULT_BASE_BRANCH = "develop";
113
+ process.env.VITE_BASE_PATH = "/base/";
114
+ process.env.GH_PATH = "/usr/bin/gh";
115
+ const { config } = await import("./config.js");
116
+ expect(config.defaultBaseBranch).toBe("develop");
117
+ expect(config.viteBasePath).toBe("/base/");
118
+ expect(config.ghPath).toBe("/usr/bin/gh");
119
+ });
120
+ it("should throw when viteBasePath is missing", async () => {
121
+ delete process.env.VITE_BASE_PATH;
122
+ process.env.NODE_ENV = "production";
123
+ process.env.CI = "false";
124
+ vi.spyOn(console, "warn").mockImplementation(() => { });
125
+ const { config, initializeConfig } = await import("./config.js");
126
+ initializeConfig();
127
+ expect(() => config.viteBasePath).toThrow("VITE_BASE_PATH must be set via environment variable or project_config.json");
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { sanitizeError } from "./error_utils.js";
3
+ describe("error_utils", () => {
4
+ describe("sanitizeError", () => {
5
+ it("should return 'Unknown error' when stderr is empty", () => {
6
+ expect(sanitizeError("")).toBe("Unknown error");
7
+ });
8
+ it("should return only the first line of stderr", () => {
9
+ const stderr = "Error: Something failed\n at StackTrace.line1\n at StackTrace.line2";
10
+ expect(sanitizeError(stderr)).toBe("Error: Something failed");
11
+ });
12
+ it("should truncate lines exceeding 200 characters", () => {
13
+ const longLine = "A".repeat(250);
14
+ const result = sanitizeError(longLine);
15
+ expect(result.length).toBe(200);
16
+ expect(result).toBe("A".repeat(200));
17
+ });
18
+ it("should preserve first line if under 200 characters", () => {
19
+ const line = "Fatal error: unexpected token near line 5";
20
+ expect(sanitizeError(line)).toBe(line);
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createSuccessResult, createErrorResult } from "./result.js";
3
+ describe("result utils", () => {
4
+ describe("createSuccessResult", () => {
5
+ it("should format string data directly without JSON stringifying", () => {
6
+ const result = createSuccessResult("simple string message");
7
+ expect(result).toEqual({
8
+ content: [
9
+ {
10
+ type: "text",
11
+ text: "simple string message",
12
+ },
13
+ ],
14
+ });
15
+ expect(result.isError).toBeUndefined();
16
+ });
17
+ it("should format object data as pretty-printed JSON string", () => {
18
+ const objData = { foo: "bar", count: 42 };
19
+ const result = createSuccessResult(objData);
20
+ expect(result).toEqual({
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: JSON.stringify(objData, null, 2),
25
+ },
26
+ ],
27
+ });
28
+ expect(result.isError).toBeUndefined();
29
+ });
30
+ });
31
+ describe("createErrorResult", () => {
32
+ it("should format error result with message and isError set to true", () => {
33
+ const result = createErrorResult("Something went wrong");
34
+ expect(result).toEqual({
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: "Something went wrong",
39
+ },
40
+ ],
41
+ isError: true,
42
+ });
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { BoomtickMCPServer } from "./server.js";
3
+ import fs from "fs/promises";
4
+ import * as healthModule from "./tools.js";
5
+ import * as routeMapModule from "../tools/repo.get_route_map.js";
6
+ import * as prDiffModule from "../tools/github.get_pr_diff.js";
7
+ import * as ciLogsModule from "../tools/repo.read_ci_logs.js";
8
+ import * as lighthouseModule from "../tools/repo.run_lighthouse.js";
9
+ import * as playwrightModule from "../tools/repo.run_playwright.js";
10
+ vi.mock("fs/promises");
11
+ vi.mock("../config.js", () => ({
12
+ config: {
13
+ repoPath: "/mock/repo",
14
+ },
15
+ }));
16
+ describe("BoomtickMCPServer", () => {
17
+ let mcpServer;
18
+ let serverInstance;
19
+ let requestHandlers;
20
+ beforeEach(() => {
21
+ vi.resetAllMocks();
22
+ requestHandlers = new Map();
23
+ mcpServer = new BoomtickMCPServer();
24
+ serverInstance = mcpServer.server;
25
+ // Retrieve registered request handlers from SDK Server instance
26
+ // SDK Server stores request handlers in server._requestHandlers or server.requestHandlers
27
+ const handlers = serverInstance._requestHandlers || serverInstance.requestHandlers;
28
+ if (handlers) {
29
+ for (const [key, handler] of handlers.entries()) {
30
+ requestHandlers.set(key, handler);
31
+ }
32
+ }
33
+ });
34
+ it("should instantiate server with correct name and capabilities", () => {
35
+ expect(serverInstance).toBeDefined();
36
+ });
37
+ describe("ListPrompts", () => {
38
+ it("should return prompts list", async () => {
39
+ const handler = requestHandlers.get("prompts/list");
40
+ expect(handler).toBeDefined();
41
+ const result = await handler({ method: "prompts/list" });
42
+ expect(result.prompts).toBeDefined();
43
+ expect(Array.isArray(result.prompts)).toBe(true);
44
+ });
45
+ });
46
+ describe("GetPrompt", () => {
47
+ it("should read prompt file and return message", async () => {
48
+ const handler = requestHandlers.get("prompts/get");
49
+ expect(handler).toBeDefined();
50
+ vi.mocked(fs.readFile).mockResolvedValue("You are a helpful agent.");
51
+ const result = await handler({
52
+ method: "prompts/get",
53
+ params: { name: "test-agent" },
54
+ });
55
+ expect(fs.readFile).toHaveBeenCalledWith("/mock/repo/mcp/src/agents/test-agent.prompt.md", "utf-8");
56
+ expect(result).toEqual({
57
+ messages: [
58
+ {
59
+ role: "user",
60
+ content: { type: "text", text: "You are a helpful agent." },
61
+ },
62
+ ],
63
+ });
64
+ });
65
+ it("should prevent path traversal attacks", async () => {
66
+ const handler = requestHandlers.get("prompts/get");
67
+ await expect(handler({
68
+ method: "prompts/get",
69
+ params: { name: "../../../etc/passwd" },
70
+ })).rejects.toThrow("Path traversal detected");
71
+ });
72
+ it("should throw when prompt file does not exist", async () => {
73
+ const handler = requestHandlers.get("prompts/get");
74
+ vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"));
75
+ await expect(handler({
76
+ method: "prompts/get",
77
+ params: { name: "nonexistent" },
78
+ })).rejects.toThrow("Prompt not found: nonexistent");
79
+ });
80
+ });
81
+ describe("ListResources", () => {
82
+ it("should return resources list", async () => {
83
+ const handler = requestHandlers.get("resources/list");
84
+ expect(handler).toBeDefined();
85
+ const result = await handler({ method: "resources/list" });
86
+ expect(result.resources).toBeDefined();
87
+ expect(Array.isArray(result.resources)).toBe(true);
88
+ });
89
+ });
90
+ describe("ReadResource", () => {
91
+ it("should handle repo://package-json", async () => {
92
+ const handler = requestHandlers.get("resources/read");
93
+ vi.mocked(fs.readFile).mockResolvedValue('{"name": "test"}');
94
+ const result = await handler({
95
+ method: "resources/read",
96
+ params: { uri: "repo://package-json" },
97
+ });
98
+ expect(result).toEqual({
99
+ contents: [{ uri: "repo://package-json", mimeType: "application/json", text: '{"name": "test"}' }],
100
+ });
101
+ });
102
+ it("should handle repo://routes", async () => {
103
+ const handler = requestHandlers.get("resources/read");
104
+ vi.spyOn(routeMapModule, "getRouteMapHandler").mockResolvedValue({ routes: ["/"] });
105
+ const result = await handler({
106
+ method: "resources/read",
107
+ params: { uri: "repo://routes" },
108
+ });
109
+ expect(result.contents[0].uri).toBe("repo://routes");
110
+ expect(JSON.parse(result.contents[0].text)).toEqual({ routes: ["/"] });
111
+ });
112
+ it("should handle repo://design-tokens", async () => {
113
+ const handler = requestHandlers.get("resources/read");
114
+ vi.mocked(fs.readFile).mockResolvedValue("export const tokens = {};");
115
+ const result = await handler({
116
+ method: "resources/read",
117
+ params: { uri: "repo://design-tokens" },
118
+ });
119
+ expect(result).toEqual({
120
+ contents: [{ uri: "repo://design-tokens", mimeType: "text/typescript", text: "export const tokens = {};" }],
121
+ });
122
+ });
123
+ it("should handle repo://diff/:prNumber", async () => {
124
+ const handler = requestHandlers.get("resources/read");
125
+ vi.spyOn(prDiffModule, "getPrDiffHandler").mockResolvedValue({ diffText: "diff --git a b" });
126
+ const result = await handler({
127
+ method: "resources/read",
128
+ params: { uri: "repo://diff/42" },
129
+ });
130
+ expect(result).toEqual({
131
+ contents: [{ uri: "repo://diff/42", mimeType: "text/plain", text: "diff --git a b" }],
132
+ });
133
+ });
134
+ it("should handle repo://ci/:prNumber", async () => {
135
+ const handler = requestHandlers.get("resources/read");
136
+ vi.spyOn(ciLogsModule, "readCiLogsHandler").mockResolvedValue({ status: "success" });
137
+ const result = await handler({
138
+ method: "resources/read",
139
+ params: { uri: "repo://ci/42" },
140
+ });
141
+ expect(result.contents[0].uri).toBe("repo://ci/42");
142
+ expect(JSON.parse(result.contents[0].text)).toEqual({ status: "success" });
143
+ });
144
+ it("should handle repo://lighthouse/:branch", async () => {
145
+ const handler = requestHandlers.get("resources/read");
146
+ vi.spyOn(lighthouseModule, "runLighthouseHandler").mockResolvedValue({ score: 100 });
147
+ const result = await handler({
148
+ method: "resources/read",
149
+ params: { uri: "repo://lighthouse/feature-x" },
150
+ });
151
+ expect(result.contents[0].uri).toBe("repo://lighthouse/feature-x");
152
+ expect(JSON.parse(result.contents[0].text)).toEqual({ score: 100 });
153
+ });
154
+ it("should handle repo://playwright/:branch", async () => {
155
+ const handler = requestHandlers.get("resources/read");
156
+ vi.spyOn(playwrightModule, "runPlaywrightHandler").mockResolvedValue({ passed: true });
157
+ const result = await handler({
158
+ method: "resources/read",
159
+ params: { uri: "repo://playwright/feature-x" },
160
+ });
161
+ expect(result.contents[0].uri).toBe("repo://playwright/feature-x");
162
+ expect(JSON.parse(result.contents[0].text)).toEqual({ passed: true });
163
+ });
164
+ it("should throw when resource is unknown", async () => {
165
+ const handler = requestHandlers.get("resources/read");
166
+ await expect(handler({
167
+ method: "resources/read",
168
+ params: { uri: "repo://unknown" },
169
+ })).rejects.toThrow("Resource not found: repo://unknown");
170
+ });
171
+ });
172
+ describe("ListTools", () => {
173
+ it("should return tools list", async () => {
174
+ const handler = requestHandlers.get("tools/list");
175
+ expect(handler).toBeDefined();
176
+ const result = await handler({ method: "tools/list" });
177
+ expect(result.tools).toBeDefined();
178
+ expect(Array.isArray(result.tools)).toBe(true);
179
+ });
180
+ });
181
+ describe("CallTool", () => {
182
+ it("should execute registered tool handler successfully", async () => {
183
+ const handler = requestHandlers.get("tools/call");
184
+ vi.spyOn(healthModule, "healthHandler").mockResolvedValue({ status: "ok" });
185
+ const result = await handler({
186
+ method: "tools/call",
187
+ params: { name: "boomtick.health", arguments: {} },
188
+ });
189
+ expect(result).toEqual({
190
+ content: [{ type: "text", text: JSON.stringify({ status: "ok" }, null, 2) }],
191
+ });
192
+ });
193
+ it("should return error result for unknown tool", async () => {
194
+ const handler = requestHandlers.get("tools/call");
195
+ const result = await handler({
196
+ method: "tools/call",
197
+ params: { name: "nonexistent.tool", arguments: {} },
198
+ });
199
+ expect(result).toEqual({
200
+ content: [{ type: "text", text: "Tool not found: nonexistent.tool" }],
201
+ isError: true,
202
+ });
203
+ });
204
+ it("should return error result when tool execution throws error", async () => {
205
+ const handler = requestHandlers.get("tools/call");
206
+ vi.spyOn(healthModule, "healthHandler").mockRejectedValue(new Error("Health check failed"));
207
+ const result = await handler({
208
+ method: "tools/call",
209
+ params: { name: "boomtick.health", arguments: {} },
210
+ });
211
+ expect(result).toEqual({
212
+ content: [{ type: "text", text: "Health check failed" }],
213
+ isError: true,
214
+ });
215
+ });
216
+ });
217
+ });
@@ -1179,7 +1179,7 @@ export const CreateBranchInputJsonSchema = {
1179
1179
  "title": "CreateBranchInput",
1180
1180
  "type": "object"
1181
1181
  };
1182
- export const CreateIssueInputSchema = z.object({ "title": z.string().min(1), "body": z.union([z.string().min(1), z.null()]).default(null), "file": z.union([z.string().min(1), z.null()]).default(null) });
1182
+ export const CreateIssueInputSchema = z.object({ "title": z.string().min(1), "body": z.union([z.string().min(1), z.null()]).default(null), "file": z.union([z.string().min(1), z.null()]).default(null), "repo": z.union([z.string(), z.null()]).describe("The target repository override (e.g. org/repo).").default(null) });
1183
1183
  export const CreateIssueInputJsonSchema = {
1184
1184
  "properties": {
1185
1185
  "title": {
@@ -1212,6 +1212,19 @@ export const CreateIssueInputJsonSchema = {
1212
1212
  ],
1213
1213
  "default": null,
1214
1214
  "title": "File"
1215
+ },
1216
+ "repo": {
1217
+ "anyOf": [
1218
+ {
1219
+ "type": "string"
1220
+ },
1221
+ {
1222
+ "type": "null"
1223
+ }
1224
+ ],
1225
+ "default": null,
1226
+ "description": "The target repository override (e.g. org/repo).",
1227
+ "title": "Repo"
1215
1228
  }
1216
1229
  },
1217
1230
  "required": [
@@ -1336,7 +1349,7 @@ export const CreateJulesSessionInputJsonSchema = {
1336
1349
  "title": "CreateJulesSessionInput",
1337
1350
  "type": "object"
1338
1351
  };
1339
- export const CreatePullRequestInputSchema = z.object({ "title": z.string().describe("PR Title."), "body": z.string().describe("Description of changes."), "head": z.string().describe("The branch containing changes to merge."), "base": z.string().describe("The target branch to merge into.").default("main"), "draft": z.boolean().describe("Whether to create the PR as a draft.").default(false) });
1352
+ export const CreatePullRequestInputSchema = z.object({ "title": z.string().describe("PR Title."), "body": z.string().describe("Description of changes."), "head": z.string().describe("The branch containing changes to merge."), "base": z.string().describe("The target branch to merge into.").default("main"), "draft": z.boolean().describe("Whether to create the PR as a draft.").default(false), "repo": z.union([z.string(), z.null()]).describe("The target repository override (e.g. org/repo).").default(null) });
1340
1353
  export const CreatePullRequestInputJsonSchema = {
1341
1354
  "properties": {
1342
1355
  "title": {
@@ -1365,6 +1378,19 @@ export const CreatePullRequestInputJsonSchema = {
1365
1378
  "description": "Whether to create the PR as a draft.",
1366
1379
  "title": "Draft",
1367
1380
  "type": "boolean"
1381
+ },
1382
+ "repo": {
1383
+ "anyOf": [
1384
+ {
1385
+ "type": "string"
1386
+ },
1387
+ {
1388
+ "type": "null"
1389
+ }
1390
+ ],
1391
+ "default": null,
1392
+ "description": "The target repository override (e.g. org/repo).",
1393
+ "title": "Repo"
1368
1394
  }
1369
1395
  },
1370
1396
  "required": [
@@ -9,6 +9,8 @@ export async function createIssueHandler(args) {
9
9
  cmdArgs.push("--body", params.body);
10
10
  if (params.file)
11
11
  cmdArgs.push("--file", params.file);
12
+ if (params.repo)
13
+ cmdArgs.push("--repo", params.repo);
12
14
  const result = await runCommand("td-cli", cmdArgs);
13
15
  if (result.exitCode !== 0) {
14
16
  throw new Error(`Failed to create issue: ${sanitizeError(result.stderr)}`);