@arii/boomtick-mcp 0.6.1 → 0.6.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 (69) hide show
  1. package/.mcp/schemas/github.create_issue.json +72 -3
  2. package/.mcp/schemas/github.get_merge_conflict_files.json +40 -3
  3. package/.mcp/schemas/github.get_pr_diff.json +30 -3
  4. package/.mcp/schemas/github.issue_update.json +72 -3
  5. package/.mcp/schemas/github.search_open_prs.json +130 -3
  6. package/.mcp/schemas/github_create_issue.json +72 -3
  7. package/.mcp/schemas/github_get_merge_conflict_files.json +40 -3
  8. package/.mcp/schemas/github_get_pr_diff.json +30 -3
  9. package/.mcp/schemas/github_issue_update.json +72 -3
  10. package/.mcp/schemas/github_search_open_prs.json +130 -3
  11. package/.mcp/schemas/repo.check_jules_status.json +38 -0
  12. package/.mcp/schemas/repo.run_tests.json +31 -3
  13. package/.mcp/schemas/repo_check_jules_status.json +38 -0
  14. package/.mcp/schemas/repo_run_tests.json +31 -3
  15. package/CHANGELOG.md +8 -0
  16. package/README.md +22 -1
  17. package/actions/run-project-gate/action.yml +1 -1
  18. package/dist/config/dynamic.js +33 -0
  19. package/dist/config/github.js +16 -0
  20. package/dist/config/repo.js +28 -0
  21. package/dist/config.js +16 -81
  22. package/dist/http.js +10 -0
  23. package/dist/mcp/definitions.js +29 -7
  24. package/dist/mcp/http.js +18 -10
  25. package/dist/mcp/http.test.js +54 -4
  26. package/dist/mcp/server.js +35 -10
  27. package/dist/mcp/server.test.js +28 -1
  28. package/dist/tools/contract.js +77 -0
  29. package/dist/tools/github.comment_triage_summary.js +3 -10
  30. package/dist/tools/github.get_merge_conflict_files.js +4 -2
  31. package/dist/tools/github.get_pr_diff.js +3 -11
  32. package/dist/tools/jules/cancel-session.js +2 -9
  33. package/dist/tools/jules/cli.js +31 -0
  34. package/dist/tools/jules/get-messages.js +2 -9
  35. package/dist/tools/jules/list-sessions.js +4 -35
  36. package/dist/tools/jules/send-message.js +2 -9
  37. package/dist/tools/jules/session.js +26 -0
  38. package/dist/tools/jules/shared.js +2 -50
  39. package/dist/tools/jules/trigger-feedback.js +2 -10
  40. package/dist/tools/repo.check_jules_status.js +148 -0
  41. package/dist/tools/repo.check_jules_status.test.js +47 -0
  42. package/dist/tools/repo.run_tests.js +13 -1
  43. package/package.json +3 -2
  44. package/scripts/sync-contracts.ts +11 -2
  45. package/src/config/dynamic.ts +36 -0
  46. package/src/config/github.ts +16 -0
  47. package/src/config/repo.ts +36 -0
  48. package/src/config.ts +16 -89
  49. package/src/http.ts +13 -0
  50. package/src/mcp/definitions.ts +34 -6
  51. package/src/mcp/http.test.ts +65 -5
  52. package/src/mcp/http.ts +20 -14
  53. package/src/mcp/server.test.ts +34 -1
  54. package/src/mcp/server.ts +36 -11
  55. package/src/tools/contract.ts +85 -0
  56. package/src/tools/github.comment_triage_summary.ts +3 -12
  57. package/src/tools/github.get_merge_conflict_files.ts +5 -2
  58. package/src/tools/github.get_pr_diff.ts +3 -13
  59. package/src/tools/jules/cancel-session.ts +2 -11
  60. package/src/tools/jules/cli.ts +42 -0
  61. package/src/tools/jules/get-messages.ts +2 -11
  62. package/src/tools/jules/list-sessions.ts +5 -39
  63. package/src/tools/jules/send-message.ts +2 -11
  64. package/src/tools/jules/session.ts +28 -0
  65. package/src/tools/jules/shared.ts +2 -63
  66. package/src/tools/jules/trigger-feedback.ts +2 -13
  67. package/src/tools/repo.check_jules_status.test.ts +55 -0
  68. package/src/tools/repo.check_jules_status.ts +176 -0
  69. package/src/tools/repo.run_tests.ts +14 -1
@@ -2,9 +2,12 @@ import { Tool, Prompt, Resource, ResourceTemplate } from "@modelcontextprotocol/
2
2
  import {
3
3
  HealthCheckInputJsonSchema,
4
4
  SearchPRsInputJsonSchema,
5
+ SearchPRsResponseJsonSchema,
5
6
  GetPrDiffInputJsonSchema,
7
+ PrDiffResponseJsonSchema,
6
8
  CheckoutBranchInputJsonSchema,
7
9
  GetMergeConflictFilesInputJsonSchema,
10
+ GetMergeConflictFilesResponseJsonSchema,
8
11
  GetChangedFilesInputJsonSchema,
9
12
  GetPackageScriptsInputJsonSchema,
10
13
  ReadCiLogsInputJsonSchema,
@@ -12,6 +15,7 @@ import {
12
15
  CreateBranchInputJsonSchema,
13
16
  CreateRepairBranchInputJsonSchema,
14
17
  RunTestsInputJsonSchema,
18
+ RunTestsResponseJsonSchema,
15
19
  CommitPatchInputJsonSchema,
16
20
  OpenReplacementPrInputJsonSchema,
17
21
  CreatePullRequestInputJsonSchema,
@@ -19,8 +23,10 @@ import {
19
23
  GetPrInputJsonSchema,
20
24
  IssueViewInputJsonSchema,
21
25
  IssueUpdateInputJsonSchema,
26
+ IssueUpdateResponseJsonSchema,
22
27
  IssueCommentInputJsonSchema,
23
28
  CreateIssueInputJsonSchema,
29
+ CreateIssueResponseJsonSchema,
24
30
  GetCommandSchemaInputJsonSchema,
25
31
  ReadAgentContextInputJsonSchema as ReadAgentContextSchema,
26
32
  CreateJulesSessionInputJsonSchema,
@@ -78,6 +84,12 @@ export const MCP_RESOURCES: Resource[] = [
78
84
  mimeType: "application/json",
79
85
  description: "List of changed files in the repository.",
80
86
  },
87
+ {
88
+ uri: "mcp://ci/latest-run",
89
+ name: "Latest CI Run",
90
+ mimeType: "application/json",
91
+ description: "Recent CI exit statuses, workflow runs, and annotations.",
92
+ },
81
93
  {
82
94
  uri: "repo://package-json",
83
95
  name: "package.json",
@@ -130,7 +142,7 @@ const BASE_MCP_TOOLS: Tool[] = [
130
142
  name: "github.search_open_prs",
131
143
  description: "Search for open pull requests in the repository.",
132
144
  inputSchema: SearchPRsInputJsonSchema as any,
133
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
145
+ outputSchema: SearchPRsResponseJsonSchema as any,
134
146
  },
135
147
  {
136
148
  name: "repo.read_agent_context",
@@ -142,7 +154,7 @@ const BASE_MCP_TOOLS: Tool[] = [
142
154
  name: "github.get_pr_diff",
143
155
  description: "Get the diff and changed files for a pull request.",
144
156
  inputSchema: GetPrDiffInputJsonSchema as any,
145
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
157
+ outputSchema: PrDiffResponseJsonSchema as any,
146
158
  },
147
159
  {
148
160
  name: "github.checkout_branch",
@@ -154,7 +166,7 @@ const BASE_MCP_TOOLS: Tool[] = [
154
166
  name: "github.get_merge_conflict_files",
155
167
  description: "Detect files that conflict when a PR is merged with the base branch.",
156
168
  inputSchema: GetMergeConflictFilesInputJsonSchema as any,
157
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
169
+ outputSchema: GetMergeConflictFilesResponseJsonSchema as any,
158
170
  },
159
171
  {
160
172
  name: "repo.get_changed_files",
@@ -198,11 +210,27 @@ const BASE_MCP_TOOLS: Tool[] = [
198
210
  inputSchema: CreateRepairBranchInputJsonSchema as any,
199
211
  outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
200
212
  },
213
+ {
214
+ name: "repo.check_jules_status",
215
+ description: "Check or poll the status of a Jules remediation session and verify its branch or PR status.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ sessionId: { type: "string", description: "The unique ID or task ID of the Jules session." },
220
+ commitSha: { type: "string", description: "Commit SHA for idempotency key verification." },
221
+ failureLog: { type: "string", description: "Failure log content for idempotency key calculation." },
222
+ poll: { type: "boolean", description: "Whether to poll until session completion or timeout." },
223
+ timeoutMs: { type: "integer", description: "Timeout in milliseconds when polling." },
224
+ intervalMs: { type: "integer", description: "Polling interval in milliseconds." },
225
+ },
226
+ } as any,
227
+ outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
228
+ },
201
229
  {
202
230
  name: "repo.run_tests",
203
231
  description: "Run repository tests and checks.",
204
232
  inputSchema: RunTestsInputJsonSchema as any,
205
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
233
+ outputSchema: RunTestsResponseJsonSchema as any,
206
234
  },
207
235
  {
208
236
  name: "repo.commit_patch",
@@ -262,7 +290,7 @@ const BASE_MCP_TOOLS: Tool[] = [
262
290
  name: "github.issue_update",
263
291
  description: "Update a GitHub issue's body, labels, and/or state.",
264
292
  inputSchema: IssueUpdateInputJsonSchema as any,
265
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
293
+ outputSchema: IssueUpdateResponseJsonSchema as any,
266
294
  },
267
295
  {
268
296
  name: "github.issue_comment",
@@ -274,7 +302,7 @@ const BASE_MCP_TOOLS: Tool[] = [
274
302
  name: "github.create_issue",
275
303
  description: "Create a new GitHub issue.",
276
304
  inputSchema: CreateIssueInputJsonSchema as any,
277
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
305
+ outputSchema: CreateIssueResponseJsonSchema as any,
278
306
  },
279
307
  {
280
308
  name: "jules.create_session",
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import http from "node:http";
3
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
3
  import { createMcpHttpServer } from "./http.js";
4
+ import { BoomtickMCPServer } from "./server.js";
5
5
 
6
6
  describe("createMcpHttpServer", () => {
7
7
  let mcpServer: Server;
@@ -18,19 +18,48 @@ describe("createMcpHttpServer", () => {
18
18
  }
19
19
  });
20
20
 
21
- it("should respond with OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
21
+ it("should respond with RFC 9728 OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
22
22
  httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
23
23
  port = await httpServerObj.listen(0);
24
24
 
25
25
  const res = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`);
26
26
  expect(res.status).toBe(200);
27
+ expect(res.headers.get("content-type")).toContain("application/json");
28
+
27
29
  const body = await res.json();
28
30
  expect(body.resource).toBe("mcp://boomtick-mcp");
29
- expect(body.authorization_servers).toBeDefined();
31
+ expect(body.authorization_servers).toEqual(["https://auth.boomtick.dev"]);
30
32
  expect(body.scopes_supported).toEqual(["mcp:read", "mcp:write"]);
33
+ expect(body.bearer_methods_supported).toEqual(["header"]);
34
+ expect(body.resource_documentation).toBe("https://github.com/arii/boomtick");
35
+ });
36
+
37
+ it("should handle HEAD requests for /.well-known/oauth-protected-resource", async () => {
38
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
39
+ port = await httpServerObj.listen(0);
40
+
41
+ const res = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`, {
42
+ method: "HEAD",
43
+ });
44
+ expect(res.status).toBe(200);
45
+ const text = await res.text();
46
+ expect(text).toBe("");
47
+ });
48
+
49
+ it("should handle OPTIONS preflight CORS requests", async () => {
50
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
51
+ port = await httpServerObj.listen(0);
52
+
53
+ const res = await fetch(`http://localhost:${port}/sse`, {
54
+ method: "OPTIONS",
55
+ });
56
+ expect(res.status).toBe(204);
57
+ expect(res.headers.get("access-control-allow-origin")).toBe("*");
58
+ expect(res.headers.get("access-control-allow-methods")).toContain("GET");
59
+ expect(res.headers.get("access-control-allow-headers")).toContain("Authorization");
31
60
  });
32
61
 
33
- it("should reject requests when Bearer token is missing or invalid", async () => {
62
+ it("should reject protected requests when Bearer token is missing or invalid", async () => {
34
63
  httpServerObj = createMcpHttpServer({ port: 0, bearerToken: "secret123", mcpServer });
35
64
  port = await httpServerObj.listen(0);
36
65
 
@@ -38,6 +67,7 @@ describe("createMcpHttpServer", () => {
38
67
  const res1 = await fetch(`http://localhost:${port}/sse`);
39
68
  expect(res1.status).toBe(401);
40
69
  expect(res1.headers.get("www-authenticate")).toContain('Bearer realm="boomtick-mcp"');
70
+ expect(res1.headers.get("www-authenticate")).toContain('error="invalid_token"');
41
71
 
42
72
  // Request with wrong token
43
73
  const res2 = await fetch(`http://localhost:${port}/sse`, {
@@ -45,7 +75,7 @@ describe("createMcpHttpServer", () => {
45
75
  });
46
76
  expect(res2.status).toBe(401);
47
77
 
48
- // Request with valid token
78
+ // Public resource metadata accessible with or without token
49
79
  const res3 = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`, {
50
80
  headers: { Authorization: "Bearer secret123" },
51
81
  });
@@ -69,4 +99,34 @@ describe("createMcpHttpServer", () => {
69
99
  const body = await res.json();
70
100
  expect(body.error).toContain("Server misconfiguration");
71
101
  });
102
+
103
+ it("should return 400 for message requests without session or invalid session", async () => {
104
+ httpServerObj = createMcpHttpServer({ port: 0, bearerToken: "secret123", mcpServer });
105
+ port = await httpServerObj.listen(0);
106
+
107
+ const res = await fetch(`http://localhost:${port}/messages?sessionId=invalid_session`, {
108
+ method: "POST",
109
+ headers: {
110
+ Authorization: "Bearer secret123",
111
+ "Content-Type": "application/json",
112
+ },
113
+ body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
114
+ });
115
+
116
+ expect(res.status).toBe(400);
117
+ const body = await res.json();
118
+ expect(body.error).toContain("Session not found or expired");
119
+ });
120
+
121
+ it("should allow BoomtickMCPServer to start HTTP server via run() when transport is http", async () => {
122
+ process.env.MCP_TRANSPORT = "http";
123
+ process.env.PORT = "0";
124
+ process.env.BOOMTICK_MCP_BEARER_TOKEN = "test-token";
125
+
126
+ const boomtickServer = new BoomtickMCPServer();
127
+ await boomtickServer.run();
128
+
129
+ // Verify instance initialized properly
130
+ expect(boomtickServer).toBeInstanceOf(BoomtickMCPServer);
131
+ });
72
132
  });
package/src/mcp/http.ts CHANGED
@@ -21,7 +21,7 @@ export function createMcpHttpServer(options: HttpServerOptions) {
21
21
  const server = http.createServer(async (req, res) => {
22
22
  // CORS Headers
23
23
  res.setHeader("Access-Control-Allow-Origin", "*");
24
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
24
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD");
25
25
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
26
26
 
27
27
  if (req.method === "OPTIONS") {
@@ -32,22 +32,28 @@ export function createMcpHttpServer(options: HttpServerOptions) {
32
32
 
33
33
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
34
34
 
35
- // Endpoint 1: OAuth Protected Resource metadata
36
- if (url.pathname === "/.well-known/oauth-protected-resource" && req.method === "GET") {
35
+ // Endpoint 1: RFC 9728 OAuth Protected Resource Metadata Discovery
36
+ if (url.pathname === "/.well-known/oauth-protected-resource" && (req.method === "GET" || req.method === "HEAD")) {
37
+ const metadata = {
38
+ resource: process.env.MCP_RESOURCE_URI || process.env.BOOMTICK_RESOURCE_URI || "mcp://boomtick-mcp",
39
+ authorization_servers: [
40
+ process.env.MCP_OAUTH_AUTH_SERVER || "https://auth.boomtick.dev",
41
+ ],
42
+ scopes_supported: ["mcp:read", "mcp:write"],
43
+ bearer_methods_supported: ["header"],
44
+ resource_documentation: process.env.MCP_RESOURCE_DOCS || "https://github.com/arii/boomtick",
45
+ };
46
+
37
47
  res.writeHead(200, { "Content-Type": "application/json" });
38
- res.end(
39
- JSON.stringify({
40
- resource: "mcp://boomtick-mcp",
41
- "authorization_servers": [
42
- process.env.MCP_OAUTH_AUTH_SERVER || "https://auth.boomtick.dev",
43
- ],
44
- scopes_supported: ["mcp:read", "mcp:write"],
45
- })
46
- );
48
+ if (req.method === "HEAD") {
49
+ res.end();
50
+ } else {
51
+ res.end(JSON.stringify(metadata, null, 2));
52
+ }
47
53
  return;
48
54
  }
49
55
 
50
- // Bearer Token Validation for SSE & Messages endpoints
56
+ // Bearer Token / OAuth 2.1 Validation for Protected Routes (/sse & /messages)
51
57
  if (url.pathname === "/sse" || url.pathname === "/messages") {
52
58
  if (!bearerToken) {
53
59
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -67,7 +73,7 @@ export function createMcpHttpServer(options: HttpServerOptions) {
67
73
  if (!isMatch) {
68
74
  res.writeHead(401, {
69
75
  "Content-Type": "application/json",
70
- "WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token"',
76
+ "WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token", error_description="Invalid or missing Bearer token"',
71
77
  });
72
78
  res.end(JSON.stringify({ error: "unauthorized", message: "Invalid or missing Bearer token" }));
73
79
  return;
@@ -100,12 +100,16 @@ describe("BoomtickMCPServer", () => {
100
100
  });
101
101
 
102
102
  describe("ListResources", () => {
103
- it("should return resources list", async () => {
103
+ it("should return resources list containing workspace resources", async () => {
104
104
  const handler = requestHandlers.get("resources/list");
105
105
  expect(handler).toBeDefined();
106
106
  const result = await handler!({ method: "resources/list" });
107
107
  expect(result.resources).toBeDefined();
108
108
  expect(Array.isArray(result.resources)).toBe(true);
109
+ const uris = result.resources.map((r: any) => r.uri);
110
+ expect(uris).toContain("mcp://workspace/agent-contract");
111
+ expect(uris).toContain("mcp://git/changed-files");
112
+ expect(uris).toContain("mcp://ci/latest-run");
109
113
  });
110
114
 
111
115
  it("should return resource templates list", async () => {
@@ -188,6 +192,35 @@ describe("BoomtickMCPServer", () => {
188
192
  expect(JSON.parse(result.contents[0].text)).toEqual({ status: "success" });
189
193
  });
190
194
 
195
+ it("should handle mcp://git/changed-files", async () => {
196
+ const handler = requestHandlers.get("resources/read");
197
+ const changedFilesModule = await import("../tools/repo.get_changed_files.js");
198
+ vi.spyOn(changedFilesModule, "getChangedFilesHandler").mockResolvedValue({ files: ["src/index.ts"] } as any);
199
+
200
+ const result = await handler!({
201
+ method: "resources/read",
202
+ params: { uri: "mcp://git/changed-files" },
203
+ });
204
+
205
+ expect(result.contents[0].uri).toBe("mcp://git/changed-files");
206
+ expect(result.contents[0].mimeType).toBe("application/json");
207
+ expect(JSON.parse(result.contents[0].text)).toEqual({ files: ["src/index.ts"] });
208
+ });
209
+
210
+ it("should handle mcp://ci/latest-run", async () => {
211
+ const handler = requestHandlers.get("resources/read");
212
+ vi.spyOn(workflowRunsModule, "getWorkflowRunsHandler").mockResolvedValue({ workflowRuns: [{ id: 1, status: "completed", conclusion: "success" }] } as any);
213
+
214
+ const result = await handler!({
215
+ method: "resources/read",
216
+ params: { uri: "mcp://ci/latest-run" },
217
+ });
218
+
219
+ expect(result.contents[0].uri).toBe("mcp://ci/latest-run");
220
+ expect(result.contents[0].mimeType).toBe("application/json");
221
+ expect(JSON.parse(result.contents[0].text)).toEqual({ workflowRuns: [{ id: 1, status: "completed", conclusion: "success" }] });
222
+ });
223
+
191
224
  it("should throw when resource is unknown", async () => {
192
225
  const handler = requestHandlers.get("resources/read");
193
226
 
package/src/mcp/server.ts CHANGED
@@ -28,6 +28,7 @@ import { getWorkflowRunsHandler, GetWorkflowRunsInputSchema } from "../tools/rep
28
28
  import { createRepairBranchHandler, CreateRepairBranchInputSchema } from "../tools/repo.create_repair_branch.js";
29
29
  import { createBranchHandler, CreateBranchInputSchema } from "../tools/repo.create_branch.js";
30
30
  import { runTestsHandler, RunTestsInputSchema } from "../tools/repo.run_tests.js";
31
+ import { checkJulesStatusHandler, CheckJulesStatusInputSchema } from "../tools/repo.check_jules_status.js";
31
32
  import { commitPatchHandler, CommitPatchInputSchema } from "../tools/repo.commit_patch.js";
32
33
  import { openReplacementPrHandler, OpenReplacementPrInputSchema } from "../tools/github.open_replacement_pr.js";
33
34
  import { commentTriageSummaryHandler, CommentTriageSummaryInputSchema } from "../tools/github.comment_triage_summary.js";
@@ -150,6 +151,16 @@ export class BoomtickMCPServer {
150
151
  contents: [{ uri, mimeType: "application/json", text: JSON.stringify(changedFiles, null, 2) }],
151
152
  };
152
153
  }
154
+ if (
155
+ uri === "mcp://ci/latest-run" ||
156
+ uri === "repo://ci/latest-run" ||
157
+ uri === "repo://ci-latest-run"
158
+ ) {
159
+ const workflowRuns = await getWorkflowRunsHandler(GetWorkflowRunsInputSchema.parse({}));
160
+ return {
161
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(workflowRuns, null, 2) }],
162
+ };
163
+ }
153
164
  if (uri === "repo://package-json" || uri === "mcp://workspace/package-json") {
154
165
  const content = await fs.readFile(path.join(config.repoPath, "package.json"), "utf-8");
155
166
  return {
@@ -234,6 +245,9 @@ export class BoomtickMCPServer {
234
245
  case "repo.create.repair.branch":
235
246
  case "repo.create_repair_branch":
236
247
  return createSuccessResult(await createRepairBranchHandler(CreateRepairBranchInputSchema.parse(normArgs)));
248
+ case "repo.check.jules.status":
249
+ case "repo.check_jules_status":
250
+ return createSuccessResult(await checkJulesStatusHandler(CheckJulesStatusInputSchema.parse(normArgs)));
237
251
  case "repo.run.tests":
238
252
  case "repo.run_tests":
239
253
  return createSuccessResult(await runTestsHandler(RunTestsInputSchema.parse(normArgs)));
@@ -383,24 +397,35 @@ export class BoomtickMCPServer {
383
397
  }
384
398
  }
385
399
 
386
- async startHttp(port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10), bearerToken?: string) {
387
- this.prepareEnvironmentAndPreflight();
388
- const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
389
- return await mcpHttp.listen(port);
390
- }
391
-
392
400
  async run() {
393
401
  this.prepareEnvironmentAndPreflight();
394
402
 
403
+ const args = process.argv.slice(2);
404
+ let port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
405
+ let bearerToken = process.env.BOOMTICK_MCP_BEARER_TOKEN || process.env.MCP_BEARER_TOKEN || process.env.AUTH_TOKEN;
406
+
407
+ for (let i = 0; i < args.length; i++) {
408
+ if (args[i] === "--port" && i + 1 < args.length) {
409
+ port = parseInt(args[i + 1], 10);
410
+ i++;
411
+ } else if (args[i].startsWith("--port=")) {
412
+ port = parseInt(args[i].split("=")[1], 10);
413
+ } else if (args[i] === "--bearer-token" && i + 1 < args.length) {
414
+ bearerToken = args[i + 1];
415
+ i++;
416
+ } else if (args[i].startsWith("--bearer-token=")) {
417
+ bearerToken = args[i].split("=")[1];
418
+ }
419
+ }
420
+
395
421
  const isHttp =
396
422
  process.env.MCP_TRANSPORT === "http" ||
397
- process.argv.includes("--http") ||
398
- (process.env.PORT !== undefined && !process.argv.includes("--stdio")) ||
399
- (process.env.HTTP_PORT !== undefined && !process.argv.includes("--stdio"));
423
+ args.includes("--http") ||
424
+ (process.env.PORT !== undefined && !args.includes("--stdio")) ||
425
+ (process.env.HTTP_PORT !== undefined && !args.includes("--stdio"));
400
426
 
401
427
  if (isHttp) {
402
- const port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
403
- const mcpHttp = createMcpHttpServer({ port, mcpServer: this.server });
428
+ const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
404
429
  await mcpHttp.listen(port);
405
430
  return;
406
431
  }
@@ -1645,6 +1645,53 @@ export const GetMergeConflictFilesInputJsonSchema = {
1645
1645
  "type": "object"
1646
1646
  } as const;
1647
1647
 
1648
+ export const GetMergeConflictFilesResponseSchema = z.object({ "hasConflicts": z.boolean(), "conflictedFiles": z.array(z.string()).optional(), "prNumber": z.union([z.number().int(), z.null()]).default(null), "baseBranch": z.string(), "headRef": z.string() });
1649
+
1650
+ export type GetMergeConflictFilesResponse = z.infer<typeof GetMergeConflictFilesResponseSchema>;
1651
+
1652
+ export const GetMergeConflictFilesResponseJsonSchema = {
1653
+ "properties": {
1654
+ "hasConflicts": {
1655
+ "title": "Hasconflicts",
1656
+ "type": "boolean"
1657
+ },
1658
+ "conflictedFiles": {
1659
+ "items": {
1660
+ "type": "string"
1661
+ },
1662
+ "title": "Conflictedfiles",
1663
+ "type": "array"
1664
+ },
1665
+ "prNumber": {
1666
+ "anyOf": [
1667
+ {
1668
+ "type": "integer"
1669
+ },
1670
+ {
1671
+ "type": "null"
1672
+ }
1673
+ ],
1674
+ "default": null,
1675
+ "title": "Prnumber"
1676
+ },
1677
+ "baseBranch": {
1678
+ "title": "Basebranch",
1679
+ "type": "string"
1680
+ },
1681
+ "headRef": {
1682
+ "title": "Headref",
1683
+ "type": "string"
1684
+ }
1685
+ },
1686
+ "required": [
1687
+ "hasConflicts",
1688
+ "baseBranch",
1689
+ "headRef"
1690
+ ],
1691
+ "title": "GetMergeConflictFilesResponse",
1692
+ "type": "object"
1693
+ } as const;
1694
+
1648
1695
  export const GetPackageScriptsInputSchema = z.object({ "filter": z.union([z.string(), z.null()]).describe("Optional glob pattern to filter script names.").default(null) });
1649
1696
 
1650
1697
  export type GetPackageScriptsInput = z.infer<typeof GetPackageScriptsInputSchema>;
@@ -3291,6 +3338,44 @@ export const RunTestsInputJsonSchema = {
3291
3338
  "type": "object"
3292
3339
  } as const;
3293
3340
 
3341
+ export const RunTestsResponseSchema = z.object({ "passed": z.boolean(), "exitCode": z.number().int(), "failedTests": z.array(z.string()).optional(), "results": z.array(z.record(z.string(), z.any())).optional() });
3342
+
3343
+ export type RunTestsResponse = z.infer<typeof RunTestsResponseSchema>;
3344
+
3345
+ export const RunTestsResponseJsonSchema = {
3346
+ "properties": {
3347
+ "passed": {
3348
+ "title": "Passed",
3349
+ "type": "boolean"
3350
+ },
3351
+ "exitCode": {
3352
+ "title": "Exitcode",
3353
+ "type": "integer"
3354
+ },
3355
+ "failedTests": {
3356
+ "items": {
3357
+ "type": "string"
3358
+ },
3359
+ "title": "Failedtests",
3360
+ "type": "array"
3361
+ },
3362
+ "results": {
3363
+ "items": {
3364
+ "additionalProperties": true,
3365
+ "type": "object"
3366
+ },
3367
+ "title": "Results",
3368
+ "type": "array"
3369
+ }
3370
+ },
3371
+ "required": [
3372
+ "passed",
3373
+ "exitCode"
3374
+ ],
3375
+ "title": "RunTestsResponse",
3376
+ "type": "object"
3377
+ } as const;
3378
+
3294
3379
  export const SearchDdgsInputSchema = z.object({ "query": z.string().describe("The search query."), "maxResults": z.union([z.number().int(), z.null()]).describe("Maximum number of results to return.").default(null) });
3295
3380
 
3296
3381
  export type SearchDdgsInput = z.infer<typeof SearchDdgsInputSchema>;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { runCommand } from "../lib/shell.js";
2
+ import { runTDCli } from "./jules/cli.js";
3
3
 
4
4
  export const CommentTriageSummaryInputSchema = z.object({
5
5
  prNumber: z.number(),
@@ -7,21 +7,12 @@ export const CommentTriageSummaryInputSchema = z.object({
7
7
  });
8
8
 
9
9
  export async function commentTriageSummaryHandler(args: z.infer<typeof CommentTriageSummaryInputSchema>) {
10
- const result = await runCommand("td", [
10
+ const output = await runTDCli([
11
11
  "gh",
12
12
  "post-comment",
13
13
  "--pr", args.prNumber.toString(),
14
14
  "--body", args.body
15
- ]);
16
-
17
- if (result.exitCode !== 0) {
18
- throw new Error(`Failed to comment on PR: ${result.stderr}`);
19
- }
20
-
21
- const output = JSON.parse(result.stdout);
22
- if (output.status === "error") {
23
- throw new Error(`Failed to comment on PR: ${output.message}`);
24
- }
15
+ ], "comment on PR");
25
16
 
26
17
  return {
27
18
  success: true,
@@ -12,10 +12,13 @@ export async function getMergeConflictFilesHandler(args: z.infer<typeof GetMerge
12
12
  const headRef = args.headRef || "HEAD";
13
13
  const conflictFiles = await getMergeConflictFiles(baseBranch, headRef);
14
14
 
15
+ const hasConflicts = conflictFiles.length > 0;
16
+
15
17
  return {
16
- prNumber: args.prNumber,
18
+ hasConflicts,
19
+ conflictedFiles: conflictFiles,
20
+ prNumber: args.prNumber ?? null,
17
21
  baseBranch,
18
22
  headRef,
19
- conflictFiles,
20
23
  };
21
24
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- import { runCommand } from "../lib/shell.js";
3
2
  import { resolveRepo } from "../lib/git.js";
3
+ import { runTDCli } from "./jules/cli.js";
4
4
 
5
5
  export const GetPrDiffInputSchema = z.object({
6
6
  prNumber: z.number().describe("The number of the pull request to get the diff for."),
@@ -11,20 +11,10 @@ export async function getPrDiffHandler(args: z.infer<typeof GetPrDiffInputSchema
11
11
  const params = GetPrDiffInputSchema.parse(args);
12
12
  const targetRepo = resolveRepo(params.repo);
13
13
 
14
- const result = await runCommand("td", [
14
+ const output = await runTDCli([
15
15
  "gh", "pr-diff", params.prNumber.toString(), "--repo", targetRepo
16
- ]);
16
+ ], "get PR diff");
17
17
 
18
- if (result.exitCode !== 0) {
19
- throw new Error(`Failed to get PR diff: ${result.stderr}`);
20
- }
21
-
22
- const output = JSON.parse(result.stdout);
23
- if (output.status === "error") {
24
- throw new Error(`Failed to get PR diff: ${output.message}`);
25
- }
26
-
27
- // Every TypeScript tool file must be a pure routing shim
28
18
  return {
29
19
  prNumber: output.prNumber,
30
20
  files: output.files,
@@ -1,22 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { JulesSession } from "../types.js";
3
- import { runCommand } from "../../lib/shell.js";
3
+ import { runTDCli } from "./cli.js";
4
4
 
5
5
  export const CancelJulesSessionInputSchema = z.object({
6
6
  sessionId: z.string(),
7
7
  });
8
8
 
9
9
  export async function cancelJulesSessionHandler(input: z.infer<typeof CancelJulesSessionInputSchema>): Promise<JulesSession> {
10
- const result = await runCommand("td", ["agent", "cancel", input.sessionId]);
11
-
12
- if (result.exitCode !== 0) {
13
- throw new Error(`Failed to cancel session: ${result.stderr}`);
14
- }
15
-
16
- const output = JSON.parse(result.stdout);
17
- if (output.status === "error") {
18
- throw new Error(`Failed to cancel session: ${output.message}`);
19
- }
10
+ await runTDCli(["agent", "cancel", input.sessionId], "cancel session");
20
11
 
21
12
  return {
22
13
  id: input.sessionId.replace("sessions/", ""),
@@ -0,0 +1,42 @@
1
+ import { runCommand } from "../../lib/shell.js";
2
+
3
+ export interface TDCliResponse {
4
+ status?: string;
5
+ message?: string;
6
+ [key: string]: any;
7
+ }
8
+
9
+ function tryParseJson(text: string): TDCliResponse | null {
10
+ try {
11
+ return JSON.parse(text) as TDCliResponse;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ export async function runTDCli(args: string[], actionName?: string): Promise<TDCliResponse> {
18
+ const result = await runCommand("td", args);
19
+ const stdout = result.stdout.trim();
20
+ const output = stdout.startsWith("{") ? tryParseJson(stdout) : null;
21
+
22
+ if (output?.status === "error") {
23
+ const errorMsg = output.message ?? "Unknown error";
24
+ if (actionName) {
25
+ throw new Error(`Failed to ${actionName}: ${errorMsg}`);
26
+ }
27
+ throw new Error(`td returned error: ${errorMsg}`);
28
+ }
29
+
30
+ if (result.exitCode !== 0) {
31
+ if (actionName) {
32
+ throw new Error(`Failed to ${actionName}: ${result.stderr || stdout}`);
33
+ }
34
+ throw new Error(`td command failed (${args.join(" ")}): ${result.stderr || stdout}`);
35
+ }
36
+
37
+ if (!output) {
38
+ throw new Error(`td returned non-JSON output with exit code 0: ${stdout}`);
39
+ }
40
+
41
+ return output;
42
+ }