@arii/boomtick-mcp 0.6.0 → 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 (75) 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 +15 -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 +47 -10
  27. package/dist/mcp/server.test.js +52 -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_commits.js +1 -1
  32. package/dist/tools/github.get_pr_diff.js +3 -11
  33. package/dist/tools/github.get_pr_reviews.js +1 -1
  34. package/dist/tools/jules/cancel-session.js +2 -9
  35. package/dist/tools/jules/cli.js +31 -0
  36. package/dist/tools/jules/get-messages.js +2 -9
  37. package/dist/tools/jules/list-sessions.js +4 -35
  38. package/dist/tools/jules/send-message.js +2 -9
  39. package/dist/tools/jules/session.js +26 -0
  40. package/dist/tools/jules/shared.js +2 -50
  41. package/dist/tools/jules/trigger-feedback.js +2 -10
  42. package/dist/tools/repo.check_jules_status.js +148 -0
  43. package/dist/tools/repo.check_jules_status.test.js +47 -0
  44. package/dist/tools/repo.get_workflow_runs.js +1 -1
  45. package/dist/tools/repo.run_tests.js +13 -1
  46. package/package.json +3 -2
  47. package/scripts/sync-contracts.ts +11 -2
  48. package/src/config/dynamic.ts +36 -0
  49. package/src/config/github.ts +16 -0
  50. package/src/config/repo.ts +36 -0
  51. package/src/config.ts +16 -89
  52. package/src/http.ts +13 -0
  53. package/src/mcp/definitions.ts +34 -6
  54. package/src/mcp/http.test.ts +65 -5
  55. package/src/mcp/http.ts +20 -14
  56. package/src/mcp/server.test.ts +62 -1
  57. package/src/mcp/server.ts +48 -11
  58. package/src/tools/contract.ts +85 -0
  59. package/src/tools/github.comment_triage_summary.ts +3 -12
  60. package/src/tools/github.get_merge_conflict_files.ts +5 -2
  61. package/src/tools/github.get_pr_commits.ts +1 -1
  62. package/src/tools/github.get_pr_diff.ts +3 -13
  63. package/src/tools/github.get_pr_reviews.ts +1 -1
  64. package/src/tools/jules/cancel-session.ts +2 -11
  65. package/src/tools/jules/cli.ts +42 -0
  66. package/src/tools/jules/get-messages.ts +2 -11
  67. package/src/tools/jules/list-sessions.ts +5 -39
  68. package/src/tools/jules/send-message.ts +2 -11
  69. package/src/tools/jules/session.ts +28 -0
  70. package/src/tools/jules/shared.ts +2 -63
  71. package/src/tools/jules/trigger-feedback.ts +2 -13
  72. package/src/tools/repo.check_jules_status.test.ts +55 -0
  73. package/src/tools/repo.check_jules_status.ts +176 -0
  74. package/src/tools/repo.get_workflow_runs.ts +1 -1
  75. package/src/tools/repo.run_tests.ts +14 -1
@@ -1,4 +1,4 @@
1
- import { HealthCheckInputJsonSchema, SearchPRsInputJsonSchema, GetPrDiffInputJsonSchema, CheckoutBranchInputJsonSchema, GetMergeConflictFilesInputJsonSchema, GetChangedFilesInputJsonSchema, GetPackageScriptsInputJsonSchema, ReadCiLogsInputJsonSchema, RepoLogsInputJsonSchema, CreateBranchInputJsonSchema, CreateRepairBranchInputJsonSchema, RunTestsInputJsonSchema, CommitPatchInputJsonSchema, OpenReplacementPrInputJsonSchema, CreatePullRequestInputJsonSchema, CommentTriageSummaryInputJsonSchema, GetPrInputJsonSchema, IssueViewInputJsonSchema, IssueUpdateInputJsonSchema, IssueCommentInputJsonSchema, CreateIssueInputJsonSchema, GetCommandSchemaInputJsonSchema, ReadAgentContextInputJsonSchema as ReadAgentContextSchema, CreateJulesSessionInputJsonSchema, JulesSessionIdInputJsonSchema, JulesSendMessageInputJsonSchema, JulesListSessionsInputJsonSchema, GetPrReviewsInputJsonSchema, GetPrCommitsInputJsonSchema, GetWorkflowRunsInputJsonSchema, } from "../tools/contract.js";
1
+ import { HealthCheckInputJsonSchema, SearchPRsInputJsonSchema, SearchPRsResponseJsonSchema, GetPrDiffInputJsonSchema, PrDiffResponseJsonSchema, CheckoutBranchInputJsonSchema, GetMergeConflictFilesInputJsonSchema, GetMergeConflictFilesResponseJsonSchema, GetChangedFilesInputJsonSchema, GetPackageScriptsInputJsonSchema, ReadCiLogsInputJsonSchema, RepoLogsInputJsonSchema, CreateBranchInputJsonSchema, CreateRepairBranchInputJsonSchema, RunTestsInputJsonSchema, RunTestsResponseJsonSchema, CommitPatchInputJsonSchema, OpenReplacementPrInputJsonSchema, CreatePullRequestInputJsonSchema, CommentTriageSummaryInputJsonSchema, GetPrInputJsonSchema, IssueViewInputJsonSchema, IssueUpdateInputJsonSchema, IssueUpdateResponseJsonSchema, IssueCommentInputJsonSchema, CreateIssueInputJsonSchema, CreateIssueResponseJsonSchema, GetCommandSchemaInputJsonSchema, ReadAgentContextInputJsonSchema as ReadAgentContextSchema, CreateJulesSessionInputJsonSchema, JulesSessionIdInputJsonSchema, JulesSendMessageInputJsonSchema, JulesListSessionsInputJsonSchema, GetPrReviewsInputJsonSchema, GetPrCommitsInputJsonSchema, GetWorkflowRunsInputJsonSchema, } from "../tools/contract.js";
2
2
  export const MCP_PROMPTS = [
3
3
  {
4
4
  name: "conflict-scout",
@@ -44,6 +44,12 @@ export const MCP_RESOURCES = [
44
44
  mimeType: "application/json",
45
45
  description: "List of changed files in the repository.",
46
46
  },
47
+ {
48
+ uri: "mcp://ci/latest-run",
49
+ name: "Latest CI Run",
50
+ mimeType: "application/json",
51
+ description: "Recent CI exit statuses, workflow runs, and annotations.",
52
+ },
47
53
  {
48
54
  uri: "repo://package-json",
49
55
  name: "package.json",
@@ -93,7 +99,7 @@ const BASE_MCP_TOOLS = [
93
99
  name: "github.search_open_prs",
94
100
  description: "Search for open pull requests in the repository.",
95
101
  inputSchema: SearchPRsInputJsonSchema,
96
- outputSchema: DEFAULT_OUTPUT_SCHEMA,
102
+ outputSchema: SearchPRsResponseJsonSchema,
97
103
  },
98
104
  {
99
105
  name: "repo.read_agent_context",
@@ -105,7 +111,7 @@ const BASE_MCP_TOOLS = [
105
111
  name: "github.get_pr_diff",
106
112
  description: "Get the diff and changed files for a pull request.",
107
113
  inputSchema: GetPrDiffInputJsonSchema,
108
- outputSchema: DEFAULT_OUTPUT_SCHEMA,
114
+ outputSchema: PrDiffResponseJsonSchema,
109
115
  },
110
116
  {
111
117
  name: "github.checkout_branch",
@@ -117,7 +123,7 @@ const BASE_MCP_TOOLS = [
117
123
  name: "github.get_merge_conflict_files",
118
124
  description: "Detect files that conflict when a PR is merged with the base branch.",
119
125
  inputSchema: GetMergeConflictFilesInputJsonSchema,
120
- outputSchema: DEFAULT_OUTPUT_SCHEMA,
126
+ outputSchema: GetMergeConflictFilesResponseJsonSchema,
121
127
  },
122
128
  {
123
129
  name: "repo.get_changed_files",
@@ -161,11 +167,27 @@ const BASE_MCP_TOOLS = [
161
167
  inputSchema: CreateRepairBranchInputJsonSchema,
162
168
  outputSchema: DEFAULT_OUTPUT_SCHEMA,
163
169
  },
170
+ {
171
+ name: "repo.check_jules_status",
172
+ description: "Check or poll the status of a Jules remediation session and verify its branch or PR status.",
173
+ inputSchema: {
174
+ type: "object",
175
+ properties: {
176
+ sessionId: { type: "string", description: "The unique ID or task ID of the Jules session." },
177
+ commitSha: { type: "string", description: "Commit SHA for idempotency key verification." },
178
+ failureLog: { type: "string", description: "Failure log content for idempotency key calculation." },
179
+ poll: { type: "boolean", description: "Whether to poll until session completion or timeout." },
180
+ timeoutMs: { type: "integer", description: "Timeout in milliseconds when polling." },
181
+ intervalMs: { type: "integer", description: "Polling interval in milliseconds." },
182
+ },
183
+ },
184
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
185
+ },
164
186
  {
165
187
  name: "repo.run_tests",
166
188
  description: "Run repository tests and checks.",
167
189
  inputSchema: RunTestsInputJsonSchema,
168
- outputSchema: DEFAULT_OUTPUT_SCHEMA,
190
+ outputSchema: RunTestsResponseJsonSchema,
169
191
  },
170
192
  {
171
193
  name: "repo.commit_patch",
@@ -225,7 +247,7 @@ const BASE_MCP_TOOLS = [
225
247
  name: "github.issue_update",
226
248
  description: "Update a GitHub issue's body, labels, and/or state.",
227
249
  inputSchema: IssueUpdateInputJsonSchema,
228
- outputSchema: DEFAULT_OUTPUT_SCHEMA,
250
+ outputSchema: IssueUpdateResponseJsonSchema,
229
251
  },
230
252
  {
231
253
  name: "github.issue_comment",
@@ -237,7 +259,7 @@ const BASE_MCP_TOOLS = [
237
259
  name: "github.create_issue",
238
260
  description: "Create a new GitHub issue.",
239
261
  inputSchema: CreateIssueInputJsonSchema,
240
- outputSchema: DEFAULT_OUTPUT_SCHEMA,
262
+ outputSchema: CreateIssueResponseJsonSchema,
241
263
  },
242
264
  {
243
265
  name: "jules.create_session",
package/dist/mcp/http.js CHANGED
@@ -7,7 +7,7 @@ export function createMcpHttpServer(options) {
7
7
  const server = http.createServer(async (req, res) => {
8
8
  // CORS Headers
9
9
  res.setHeader("Access-Control-Allow-Origin", "*");
10
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
10
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD");
11
11
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
12
12
  if (req.method === "OPTIONS") {
13
13
  res.writeHead(204);
@@ -15,19 +15,27 @@ export function createMcpHttpServer(options) {
15
15
  return;
16
16
  }
17
17
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
18
- // Endpoint 1: OAuth Protected Resource metadata
19
- if (url.pathname === "/.well-known/oauth-protected-resource" && req.method === "GET") {
20
- res.writeHead(200, { "Content-Type": "application/json" });
21
- res.end(JSON.stringify({
22
- resource: "mcp://boomtick-mcp",
23
- "authorization_servers": [
18
+ // Endpoint 1: RFC 9728 OAuth Protected Resource Metadata Discovery
19
+ if (url.pathname === "/.well-known/oauth-protected-resource" && (req.method === "GET" || req.method === "HEAD")) {
20
+ const metadata = {
21
+ resource: process.env.MCP_RESOURCE_URI || process.env.BOOMTICK_RESOURCE_URI || "mcp://boomtick-mcp",
22
+ authorization_servers: [
24
23
  process.env.MCP_OAUTH_AUTH_SERVER || "https://auth.boomtick.dev",
25
24
  ],
26
25
  scopes_supported: ["mcp:read", "mcp:write"],
27
- }));
26
+ bearer_methods_supported: ["header"],
27
+ resource_documentation: process.env.MCP_RESOURCE_DOCS || "https://github.com/arii/boomtick",
28
+ };
29
+ res.writeHead(200, { "Content-Type": "application/json" });
30
+ if (req.method === "HEAD") {
31
+ res.end();
32
+ }
33
+ else {
34
+ res.end(JSON.stringify(metadata, null, 2));
35
+ }
28
36
  return;
29
37
  }
30
- // Bearer Token Validation for SSE & Messages endpoints
38
+ // Bearer Token / OAuth 2.1 Validation for Protected Routes (/sse & /messages)
31
39
  if (url.pathname === "/sse" || url.pathname === "/messages") {
32
40
  if (!bearerToken) {
33
41
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -44,7 +52,7 @@ export function createMcpHttpServer(options) {
44
52
  if (!isMatch) {
45
53
  res.writeHead(401, {
46
54
  "Content-Type": "application/json",
47
- "WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token"',
55
+ "WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token", error_description="Invalid or missing Bearer token"',
48
56
  });
49
57
  res.end(JSON.stringify({ error: "unauthorized", message: "Invalid or missing Bearer token" }));
50
58
  return;
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { createMcpHttpServer } from "./http.js";
4
+ import { BoomtickMCPServer } from "./server.js";
4
5
  describe("createMcpHttpServer", () => {
5
6
  let mcpServer;
6
7
  let httpServerObj;
@@ -13,29 +14,54 @@ describe("createMcpHttpServer", () => {
13
14
  httpServerObj.server.close();
14
15
  }
15
16
  });
16
- it("should respond with OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
17
+ it("should respond with RFC 9728 OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
17
18
  httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
18
19
  port = await httpServerObj.listen(0);
19
20
  const res = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`);
20
21
  expect(res.status).toBe(200);
22
+ expect(res.headers.get("content-type")).toContain("application/json");
21
23
  const body = await res.json();
22
24
  expect(body.resource).toBe("mcp://boomtick-mcp");
23
- expect(body.authorization_servers).toBeDefined();
25
+ expect(body.authorization_servers).toEqual(["https://auth.boomtick.dev"]);
24
26
  expect(body.scopes_supported).toEqual(["mcp:read", "mcp:write"]);
27
+ expect(body.bearer_methods_supported).toEqual(["header"]);
28
+ expect(body.resource_documentation).toBe("https://github.com/arii/boomtick");
25
29
  });
26
- it("should reject requests when Bearer token is missing or invalid", async () => {
30
+ it("should handle HEAD requests for /.well-known/oauth-protected-resource", async () => {
31
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
32
+ port = await httpServerObj.listen(0);
33
+ const res = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`, {
34
+ method: "HEAD",
35
+ });
36
+ expect(res.status).toBe(200);
37
+ const text = await res.text();
38
+ expect(text).toBe("");
39
+ });
40
+ it("should handle OPTIONS preflight CORS requests", async () => {
41
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
42
+ port = await httpServerObj.listen(0);
43
+ const res = await fetch(`http://localhost:${port}/sse`, {
44
+ method: "OPTIONS",
45
+ });
46
+ expect(res.status).toBe(204);
47
+ expect(res.headers.get("access-control-allow-origin")).toBe("*");
48
+ expect(res.headers.get("access-control-allow-methods")).toContain("GET");
49
+ expect(res.headers.get("access-control-allow-headers")).toContain("Authorization");
50
+ });
51
+ it("should reject protected requests when Bearer token is missing or invalid", async () => {
27
52
  httpServerObj = createMcpHttpServer({ port: 0, bearerToken: "secret123", mcpServer });
28
53
  port = await httpServerObj.listen(0);
29
54
  // Request without token
30
55
  const res1 = await fetch(`http://localhost:${port}/sse`);
31
56
  expect(res1.status).toBe(401);
32
57
  expect(res1.headers.get("www-authenticate")).toContain('Bearer realm="boomtick-mcp"');
58
+ expect(res1.headers.get("www-authenticate")).toContain('error="invalid_token"');
33
59
  // Request with wrong token
34
60
  const res2 = await fetch(`http://localhost:${port}/sse`, {
35
61
  headers: { Authorization: "Bearer wrong" },
36
62
  });
37
63
  expect(res2.status).toBe(401);
38
- // Request with valid token
64
+ // Public resource metadata accessible with or without token
39
65
  const res3 = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`, {
40
66
  headers: { Authorization: "Bearer secret123" },
41
67
  });
@@ -55,4 +81,28 @@ describe("createMcpHttpServer", () => {
55
81
  const body = await res.json();
56
82
  expect(body.error).toContain("Server misconfiguration");
57
83
  });
84
+ it("should return 400 for message requests without session or invalid session", async () => {
85
+ httpServerObj = createMcpHttpServer({ port: 0, bearerToken: "secret123", mcpServer });
86
+ port = await httpServerObj.listen(0);
87
+ const res = await fetch(`http://localhost:${port}/messages?sessionId=invalid_session`, {
88
+ method: "POST",
89
+ headers: {
90
+ Authorization: "Bearer secret123",
91
+ "Content-Type": "application/json",
92
+ },
93
+ body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
94
+ });
95
+ expect(res.status).toBe(400);
96
+ const body = await res.json();
97
+ expect(body.error).toContain("Session not found or expired");
98
+ });
99
+ it("should allow BoomtickMCPServer to start HTTP server via run() when transport is http", async () => {
100
+ process.env.MCP_TRANSPORT = "http";
101
+ process.env.PORT = "0";
102
+ process.env.BOOMTICK_MCP_BEARER_TOKEN = "test-token";
103
+ const boomtickServer = new BoomtickMCPServer();
104
+ await boomtickServer.run();
105
+ // Verify instance initialized properly
106
+ expect(boomtickServer).toBeInstanceOf(BoomtickMCPServer);
107
+ });
58
108
  });
@@ -16,14 +16,18 @@ import { getCommandSchemaHandler, GetCommandSchemaInputSchema } from "../tools/r
16
16
  import { getPackageScriptsHandler, GetPackageScriptsInputSchema } from "../tools/repo.get_package_scripts.js";
17
17
  import { readCiLogsHandler, ReadCiLogsInputSchema } from "../tools/repo.read_ci_logs.js";
18
18
  import { repoLogsHandler, RepoLogsInputSchema } from "../tools/repo.logs.js";
19
+ import { getWorkflowRunsHandler, GetWorkflowRunsInputSchema } from "../tools/repo.get_workflow_runs.js";
19
20
  import { createRepairBranchHandler, CreateRepairBranchInputSchema } from "../tools/repo.create_repair_branch.js";
20
21
  import { createBranchHandler, CreateBranchInputSchema } from "../tools/repo.create_branch.js";
21
22
  import { runTestsHandler, RunTestsInputSchema } from "../tools/repo.run_tests.js";
23
+ import { checkJulesStatusHandler, CheckJulesStatusInputSchema } from "../tools/repo.check_jules_status.js";
22
24
  import { commitPatchHandler, CommitPatchInputSchema } from "../tools/repo.commit_patch.js";
23
25
  import { openReplacementPrHandler, OpenReplacementPrInputSchema } from "../tools/github.open_replacement_pr.js";
24
26
  import { commentTriageSummaryHandler, CommentTriageSummaryInputSchema } from "../tools/github.comment_triage_summary.js";
25
27
  import { createPullRequestHandler, CreatePullRequestInputSchema } from "../tools/github.create_pull_request.js";
26
28
  import { getPrHandler, GetPrInputSchema } from "../tools/github.get_pr.js";
29
+ import { getPrReviewsHandler, GetPrReviewsInputSchema } from "../tools/github.get_pr_reviews.js";
30
+ import { getPrCommitsHandler, GetPrCommitsInputSchema } from "../tools/github.get_pr_commits.js";
27
31
  import { issueViewHandler, IssueViewInputSchema } from "../tools/github.issue_view.js";
28
32
  import { issueUpdateHandler, IssueUpdateInputSchema } from "../tools/github.issue_update.js";
29
33
  import { issueCommentHandler, IssueCommentInputSchema } from "../tools/github.issue_comment.js";
@@ -117,6 +121,14 @@ export class BoomtickMCPServer {
117
121
  contents: [{ uri, mimeType: "application/json", text: JSON.stringify(changedFiles, null, 2) }],
118
122
  };
119
123
  }
124
+ if (uri === "mcp://ci/latest-run" ||
125
+ uri === "repo://ci/latest-run" ||
126
+ uri === "repo://ci-latest-run") {
127
+ const workflowRuns = await getWorkflowRunsHandler(GetWorkflowRunsInputSchema.parse({}));
128
+ return {
129
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(workflowRuns, null, 2) }],
130
+ };
131
+ }
120
132
  if (uri === "repo://package-json" || uri === "mcp://workspace/package-json") {
121
133
  const content = await fs.readFile(path.join(config.repoPath, "package.json"), "utf-8");
122
134
  return {
@@ -189,12 +201,18 @@ export class BoomtickMCPServer {
189
201
  return createSuccessResult(await readCiLogsHandler(ReadCiLogsInputSchema.parse(normArgs)));
190
202
  case "repo.logs":
191
203
  return createSuccessResult(await repoLogsHandler(RepoLogsInputSchema.parse(normArgs)));
204
+ case "repo.get.workflow.runs":
205
+ case "repo.get_workflow_runs":
206
+ return createSuccessResult(await getWorkflowRunsHandler(GetWorkflowRunsInputSchema.parse(normArgs)));
192
207
  case "repo.create.branch":
193
208
  case "repo.create_branch":
194
209
  return createSuccessResult(await createBranchHandler(CreateBranchInputSchema.parse(normArgs)));
195
210
  case "repo.create.repair.branch":
196
211
  case "repo.create_repair_branch":
197
212
  return createSuccessResult(await createRepairBranchHandler(CreateRepairBranchInputSchema.parse(normArgs)));
213
+ case "repo.check.jules.status":
214
+ case "repo.check_jules_status":
215
+ return createSuccessResult(await checkJulesStatusHandler(CheckJulesStatusInputSchema.parse(normArgs)));
198
216
  case "repo.run.tests":
199
217
  case "repo.run_tests":
200
218
  return createSuccessResult(await runTestsHandler(RunTestsInputSchema.parse(normArgs)));
@@ -213,6 +231,12 @@ export class BoomtickMCPServer {
213
231
  case "github.get.pr":
214
232
  case "github.get_pr":
215
233
  return createSuccessResult(await getPrHandler(GetPrInputSchema.parse(normArgs)));
234
+ case "github.get.pr.reviews":
235
+ case "github.get_pr_reviews":
236
+ return createSuccessResult(await getPrReviewsHandler(GetPrReviewsInputSchema.parse(normArgs)));
237
+ case "github.get.pr.commits":
238
+ case "github.get_pr_commits":
239
+ return createSuccessResult(await getPrCommitsHandler(GetPrCommitsInputSchema.parse(normArgs)));
216
240
  case "github.issue.view":
217
241
  case "github.issue_view":
218
242
  return createSuccessResult(await issueViewHandler(IssueViewInputSchema.parse(normArgs)));
@@ -323,20 +347,33 @@ export class BoomtickMCPServer {
323
347
  process.exit(1);
324
348
  }
325
349
  }
326
- async startHttp(port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10), bearerToken) {
327
- this.prepareEnvironmentAndPreflight();
328
- const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
329
- return await mcpHttp.listen(port);
330
- }
331
350
  async run() {
332
351
  this.prepareEnvironmentAndPreflight();
352
+ const args = process.argv.slice(2);
353
+ let port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
354
+ let bearerToken = process.env.BOOMTICK_MCP_BEARER_TOKEN || process.env.MCP_BEARER_TOKEN || process.env.AUTH_TOKEN;
355
+ for (let i = 0; i < args.length; i++) {
356
+ if (args[i] === "--port" && i + 1 < args.length) {
357
+ port = parseInt(args[i + 1], 10);
358
+ i++;
359
+ }
360
+ else if (args[i].startsWith("--port=")) {
361
+ port = parseInt(args[i].split("=")[1], 10);
362
+ }
363
+ else if (args[i] === "--bearer-token" && i + 1 < args.length) {
364
+ bearerToken = args[i + 1];
365
+ i++;
366
+ }
367
+ else if (args[i].startsWith("--bearer-token=")) {
368
+ bearerToken = args[i].split("=")[1];
369
+ }
370
+ }
333
371
  const isHttp = process.env.MCP_TRANSPORT === "http" ||
334
- process.argv.includes("--http") ||
335
- (process.env.PORT !== undefined && !process.argv.includes("--stdio")) ||
336
- (process.env.HTTP_PORT !== undefined && !process.argv.includes("--stdio"));
372
+ args.includes("--http") ||
373
+ (process.env.PORT !== undefined && !args.includes("--stdio")) ||
374
+ (process.env.HTTP_PORT !== undefined && !args.includes("--stdio"));
337
375
  if (isHttp) {
338
- const port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
339
- const mcpHttp = createMcpHttpServer({ port, mcpServer: this.server });
376
+ const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
340
377
  await mcpHttp.listen(port);
341
378
  return;
342
379
  }
@@ -4,6 +4,9 @@ import fs from "fs/promises";
4
4
  import * as healthModule from "./tools.js";
5
5
  import * as prDiffModule from "../tools/github.get_pr_diff.js";
6
6
  import * as ciLogsModule from "../tools/repo.read_ci_logs.js";
7
+ import * as prReviewsModule from "../tools/github.get_pr_reviews.js";
8
+ import * as prCommitsModule from "../tools/github.get_pr_commits.js";
9
+ import * as workflowRunsModule from "../tools/repo.get_workflow_runs.js";
7
10
  vi.mock("fs/promises");
8
11
  vi.mock("../config.js", () => ({
9
12
  config: {
@@ -76,12 +79,16 @@ describe("BoomtickMCPServer", () => {
76
79
  });
77
80
  });
78
81
  describe("ListResources", () => {
79
- it("should return resources list", async () => {
82
+ it("should return resources list containing workspace resources", async () => {
80
83
  const handler = requestHandlers.get("resources/list");
81
84
  expect(handler).toBeDefined();
82
85
  const result = await handler({ method: "resources/list" });
83
86
  expect(result.resources).toBeDefined();
84
87
  expect(Array.isArray(result.resources)).toBe(true);
88
+ const uris = result.resources.map((r) => r.uri);
89
+ expect(uris).toContain("mcp://workspace/agent-contract");
90
+ expect(uris).toContain("mcp://git/changed-files");
91
+ expect(uris).toContain("mcp://ci/latest-run");
85
92
  });
86
93
  it("should return resource templates list", async () => {
87
94
  const handler = requestHandlers.get("resources/templates/list");
@@ -147,6 +154,29 @@ describe("BoomtickMCPServer", () => {
147
154
  expect(result.contents[0].uri).toBe("repo://ci/42");
148
155
  expect(JSON.parse(result.contents[0].text)).toEqual({ status: "success" });
149
156
  });
157
+ it("should handle mcp://git/changed-files", async () => {
158
+ const handler = requestHandlers.get("resources/read");
159
+ const changedFilesModule = await import("../tools/repo.get_changed_files.js");
160
+ vi.spyOn(changedFilesModule, "getChangedFilesHandler").mockResolvedValue({ files: ["src/index.ts"] });
161
+ const result = await handler({
162
+ method: "resources/read",
163
+ params: { uri: "mcp://git/changed-files" },
164
+ });
165
+ expect(result.contents[0].uri).toBe("mcp://git/changed-files");
166
+ expect(result.contents[0].mimeType).toBe("application/json");
167
+ expect(JSON.parse(result.contents[0].text)).toEqual({ files: ["src/index.ts"] });
168
+ });
169
+ it("should handle mcp://ci/latest-run", async () => {
170
+ const handler = requestHandlers.get("resources/read");
171
+ vi.spyOn(workflowRunsModule, "getWorkflowRunsHandler").mockResolvedValue({ workflowRuns: [{ id: 1, status: "completed", conclusion: "success" }] });
172
+ const result = await handler({
173
+ method: "resources/read",
174
+ params: { uri: "mcp://ci/latest-run" },
175
+ });
176
+ expect(result.contents[0].uri).toBe("mcp://ci/latest-run");
177
+ expect(result.contents[0].mimeType).toBe("application/json");
178
+ expect(JSON.parse(result.contents[0].text)).toEqual({ workflowRuns: [{ id: 1, status: "completed", conclusion: "success" }] });
179
+ });
150
180
  it("should throw when resource is unknown", async () => {
151
181
  const handler = requestHandlers.get("resources/read");
152
182
  await expect(handler({
@@ -177,6 +207,27 @@ describe("BoomtickMCPServer", () => {
177
207
  structuredContent: { status: "ok" },
178
208
  });
179
209
  });
210
+ it("should execute github_get_pr_reviews and github_get_pr_commits via underscore or dot name", async () => {
211
+ const handler = requestHandlers.get("tools/call");
212
+ vi.spyOn(prReviewsModule, "getPrReviewsHandler").mockResolvedValue({ reviews: [] });
213
+ vi.spyOn(prCommitsModule, "getPrCommitsHandler").mockResolvedValue({ commits: [] });
214
+ vi.spyOn(workflowRunsModule, "getWorkflowRunsHandler").mockResolvedValue({ runs: [] });
215
+ const resReviews = await handler({
216
+ method: "tools/call",
217
+ params: { name: "github_get_pr_reviews", arguments: { prNumber: 123 } },
218
+ });
219
+ expect(resReviews.structuredContent).toEqual({ reviews: [] });
220
+ const resCommits = await handler({
221
+ method: "tools/call",
222
+ params: { name: "github_get_pr_commits", arguments: { prNumber: 123 } },
223
+ });
224
+ expect(resCommits.structuredContent).toEqual({ commits: [] });
225
+ const resWorkflow = await handler({
226
+ method: "tools/call",
227
+ params: { name: "repo_get_workflow_runs", arguments: {} },
228
+ });
229
+ expect(resWorkflow.structuredContent).toEqual({ runs: [] });
230
+ });
180
231
  it("should return error result for unknown tool", async () => {
181
232
  const handler = requestHandlers.get("tools/call");
182
233
  const result = await handler({
@@ -1564,6 +1564,49 @@ export const GetMergeConflictFilesInputJsonSchema = {
1564
1564
  "title": "GetMergeConflictFilesInput",
1565
1565
  "type": "object"
1566
1566
  };
1567
+ 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() });
1568
+ export const GetMergeConflictFilesResponseJsonSchema = {
1569
+ "properties": {
1570
+ "hasConflicts": {
1571
+ "title": "Hasconflicts",
1572
+ "type": "boolean"
1573
+ },
1574
+ "conflictedFiles": {
1575
+ "items": {
1576
+ "type": "string"
1577
+ },
1578
+ "title": "Conflictedfiles",
1579
+ "type": "array"
1580
+ },
1581
+ "prNumber": {
1582
+ "anyOf": [
1583
+ {
1584
+ "type": "integer"
1585
+ },
1586
+ {
1587
+ "type": "null"
1588
+ }
1589
+ ],
1590
+ "default": null,
1591
+ "title": "Prnumber"
1592
+ },
1593
+ "baseBranch": {
1594
+ "title": "Basebranch",
1595
+ "type": "string"
1596
+ },
1597
+ "headRef": {
1598
+ "title": "Headref",
1599
+ "type": "string"
1600
+ }
1601
+ },
1602
+ "required": [
1603
+ "hasConflicts",
1604
+ "baseBranch",
1605
+ "headRef"
1606
+ ],
1607
+ "title": "GetMergeConflictFilesResponse",
1608
+ "type": "object"
1609
+ };
1567
1610
  export const GetPackageScriptsInputSchema = z.object({ "filter": z.union([z.string(), z.null()]).describe("Optional glob pattern to filter script names.").default(null) });
1568
1611
  export const GetPackageScriptsInputJsonSchema = {
1569
1612
  "properties": {
@@ -3086,6 +3129,40 @@ export const RunTestsInputJsonSchema = {
3086
3129
  "title": "RunTestsInput",
3087
3130
  "type": "object"
3088
3131
  };
3132
+ 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() });
3133
+ export const RunTestsResponseJsonSchema = {
3134
+ "properties": {
3135
+ "passed": {
3136
+ "title": "Passed",
3137
+ "type": "boolean"
3138
+ },
3139
+ "exitCode": {
3140
+ "title": "Exitcode",
3141
+ "type": "integer"
3142
+ },
3143
+ "failedTests": {
3144
+ "items": {
3145
+ "type": "string"
3146
+ },
3147
+ "title": "Failedtests",
3148
+ "type": "array"
3149
+ },
3150
+ "results": {
3151
+ "items": {
3152
+ "additionalProperties": true,
3153
+ "type": "object"
3154
+ },
3155
+ "title": "Results",
3156
+ "type": "array"
3157
+ }
3158
+ },
3159
+ "required": [
3160
+ "passed",
3161
+ "exitCode"
3162
+ ],
3163
+ "title": "RunTestsResponse",
3164
+ "type": "object"
3165
+ };
3089
3166
  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) });
3090
3167
  export const SearchDdgsInputJsonSchema = {
3091
3168
  "properties": {
@@ -1,23 +1,16 @@
1
1
  import { z } from "zod";
2
- import { runCommand } from "../lib/shell.js";
2
+ import { runTDCli } from "./jules/cli.js";
3
3
  export const CommentTriageSummaryInputSchema = z.object({
4
4
  prNumber: z.number(),
5
5
  body: z.string(),
6
6
  });
7
7
  export async function commentTriageSummaryHandler(args) {
8
- const result = await runCommand("td", [
8
+ const output = await runTDCli([
9
9
  "gh",
10
10
  "post-comment",
11
11
  "--pr", args.prNumber.toString(),
12
12
  "--body", args.body
13
- ]);
14
- if (result.exitCode !== 0) {
15
- throw new Error(`Failed to comment on PR: ${result.stderr}`);
16
- }
17
- const output = JSON.parse(result.stdout);
18
- if (output.status === "error") {
19
- throw new Error(`Failed to comment on PR: ${output.message}`);
20
- }
13
+ ], "comment on PR");
21
14
  return {
22
15
  success: true,
23
16
  commentUrl: output.html_url || output.url
@@ -9,10 +9,12 @@ export async function getMergeConflictFilesHandler(args) {
9
9
  const baseBranch = args.baseBranch || "main";
10
10
  const headRef = args.headRef || "HEAD";
11
11
  const conflictFiles = await getMergeConflictFiles(baseBranch, headRef);
12
+ const hasConflicts = conflictFiles.length > 0;
12
13
  return {
13
- prNumber: args.prNumber,
14
+ hasConflicts,
15
+ conflictedFiles: conflictFiles,
16
+ prNumber: args.prNumber ?? null,
14
17
  baseBranch,
15
18
  headRef,
16
- conflictFiles,
17
19
  };
18
20
  }
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { runCommand } from "../lib/shell.js";
3
3
  import { resolveRepo } from "../lib/git.js";
4
4
  import { sanitizeError } from "../lib/error_utils.js";
5
- const GetPrCommitsInputSchema = z.object({
5
+ export const GetPrCommitsInputSchema = z.object({
6
6
  prNumber: z.number().describe("The number of the PR to list commits for."),
7
7
  repo: z.string().optional().describe("The target repository override (e.g. org/repo)."),
8
8
  });
@@ -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
  export const GetPrDiffInputSchema = z.object({
5
5
  prNumber: z.number().describe("The number of the pull request to get the diff for."),
6
6
  repo: z.string().optional().describe("The target repository override (e.g. org/repo)."),
@@ -8,17 +8,9 @@ export const GetPrDiffInputSchema = z.object({
8
8
  export async function getPrDiffHandler(args) {
9
9
  const params = GetPrDiffInputSchema.parse(args);
10
10
  const targetRepo = resolveRepo(params.repo);
11
- const result = await runCommand("td", [
11
+ const output = await runTDCli([
12
12
  "gh", "pr-diff", params.prNumber.toString(), "--repo", targetRepo
13
- ]);
14
- if (result.exitCode !== 0) {
15
- throw new Error(`Failed to get PR diff: ${result.stderr}`);
16
- }
17
- const output = JSON.parse(result.stdout);
18
- if (output.status === "error") {
19
- throw new Error(`Failed to get PR diff: ${output.message}`);
20
- }
21
- // Every TypeScript tool file must be a pure routing shim
13
+ ], "get PR diff");
22
14
  return {
23
15
  prNumber: output.prNumber,
24
16
  files: output.files,
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { runCommand } from "../lib/shell.js";
3
3
  import { resolveRepo } from "../lib/git.js";
4
4
  import { sanitizeError } from "../lib/error_utils.js";
5
- const GetPrReviewsInputSchema = z.object({
5
+ export const GetPrReviewsInputSchema = z.object({
6
6
  prNumber: z.number().describe("The number of the PR to view reviews for."),
7
7
  repo: z.string().optional().describe("The target repository override (e.g. org/repo)."),
8
8
  });
@@ -1,17 +1,10 @@
1
1
  import { z } from "zod";
2
- import { runCommand } from "../../lib/shell.js";
2
+ import { runTDCli } from "./cli.js";
3
3
  export const CancelJulesSessionInputSchema = z.object({
4
4
  sessionId: z.string(),
5
5
  });
6
6
  export async function cancelJulesSessionHandler(input) {
7
- const result = await runCommand("td", ["agent", "cancel", input.sessionId]);
8
- if (result.exitCode !== 0) {
9
- throw new Error(`Failed to cancel session: ${result.stderr}`);
10
- }
11
- const output = JSON.parse(result.stdout);
12
- if (output.status === "error") {
13
- throw new Error(`Failed to cancel session: ${output.message}`);
14
- }
7
+ await runTDCli(["agent", "cancel", input.sessionId], "cancel session");
15
8
  return {
16
9
  id: input.sessionId.replace("sessions/", ""),
17
10
  status: "FAILED",