@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
package/src/config.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import { fileURLToPath } from "url";
4
- import { execSync } from "child_process";
4
+ import { findRepoRoot } from "./config/repo.js";
5
+ import { getGithubToken } from "./config/github.js";
6
+ import { initializeConfig, getCachedDynamicConfig } from "./config/dynamic.js";
5
7
 
6
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
9
 
@@ -14,93 +16,14 @@ if (fs.existsSync(envPath) && typeof process.loadEnvFile === "function") {
14
16
  }
15
17
  }
16
18
 
17
- function getGithubToken(): string | undefined {
18
- if (process.env.GITHUB_TOKEN) {
19
- return process.env.GITHUB_TOKEN;
20
- }
21
- try {
22
- const token = execSync("gh auth token", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23
- if (token) {
24
- return token;
25
- }
26
- } catch (e) { /* ignore to allow fallback */ }
27
- return undefined;
28
- }
29
-
30
- let cachedDynamicConfig: any = null;
31
-
32
- /**
33
- * Explicitly initializes dynamic configuration from the Python CLI.
34
- * Should be called once during server startup.
35
- */
36
- export function initializeConfig() {
37
- if (cachedDynamicConfig !== null) {
38
- return cachedDynamicConfig;
39
- }
40
-
41
- try {
42
- // Attempt to load core properties from the Python CLI in the active workspace
43
- const cwd = process.env.BOOMTICK_REPO_PATH || process.cwd();
44
- const cmd = `td config view`;
45
- const output = execSync(cmd, {
46
- cwd,
47
- encoding: "utf-8",
48
- stdio: ["ignore", "pipe", "ignore"]
49
- });
50
- cachedDynamicConfig = JSON.parse(output);
51
- return cachedDynamicConfig;
52
- } catch (e) {
53
- const message = e instanceof Error ? e.message : String(e);
54
- const errorPrefix = `CRITICAL: Failed to load dynamic config from Python CLI. Ensure Python 3 is installed and cli is in your PYTHONPATH. Details: ${message}`;
55
-
56
- if (process.env.NODE_ENV === "development" || process.env.CI === "true") {
57
- throw new Error(errorPrefix);
58
- } else {
59
- console.warn(errorPrefix);
60
- }
61
- // Set an empty object to prevent repeated attempts if initialization fails in non-blocking mode
62
- cachedDynamicConfig = {};
63
- }
64
- return cachedDynamicConfig;
65
- }
66
-
67
- function findRepoRoot() {
68
- if (process.env.BOOMTICK_REPO_PATH) {
69
- return process.env.BOOMTICK_REPO_PATH;
70
- }
71
-
72
- // Traverse up from process.cwd() to find project_config.json, workspace.json, or .git
73
- let current = process.cwd();
74
- while (current !== path.parse(current).root) {
75
- if (
76
- fs.existsSync(path.join(current, "project_config.json")) ||
77
- fs.existsSync(path.join(current, "workspace.json")) ||
78
- fs.existsSync(path.join(current, ".git"))
79
- ) {
80
- return current;
81
- }
82
- current = path.dirname(current);
83
- }
84
-
85
- // Fallback to __dirname search if discovery from cwd fails
86
- current = __dirname;
87
- while (current !== path.parse(current).root) {
88
- if (
89
- fs.existsSync(path.join(current, "project_config.json")) ||
90
- fs.existsSync(path.join(current, "workspace.json")) ||
91
- fs.existsSync(path.join(current, ".git"))
92
- ) {
93
- return current;
94
- }
95
- current = path.dirname(current);
96
- }
97
- // Fallback to the monolithic relative path if discovery fails
98
- return path.resolve(__dirname, "../../../");
99
- }
19
+ export { initializeConfig };
100
20
 
101
21
  export const config = {
102
- get githubToken() { return getGithubToken(); },
22
+ get githubToken() {
23
+ return getGithubToken();
24
+ },
103
25
  get githubOwner() {
26
+ const cachedDynamicConfig = getCachedDynamicConfig();
104
27
  const repoString = cachedDynamicConfig?.github_repo;
105
28
  if (typeof repoString === "string" && repoString.includes("/")) {
106
29
  return repoString.split("/")[0];
@@ -113,6 +36,7 @@ export const config = {
113
36
  throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
114
37
  },
115
38
  get githubRepo() {
39
+ const cachedDynamicConfig = getCachedDynamicConfig();
116
40
  const repoString = cachedDynamicConfig?.github_repo;
117
41
  if (typeof repoString === "string" && repoString.includes("/")) {
118
42
  return repoString.split("/")[1];
@@ -128,16 +52,19 @@ export const config = {
128
52
  return findRepoRoot();
129
53
  },
130
54
  get defaultBaseBranch() {
55
+ const cachedDynamicConfig = getCachedDynamicConfig();
131
56
  return process.env.DEFAULT_BASE_BRANCH || cachedDynamicConfig?.base_branch?.split("/").pop() || "main";
132
57
  },
133
58
  get viteBasePath() {
134
- const path = process.env.VITE_BASE_PATH || cachedDynamicConfig?.vite_base_path;
135
- if (!path) {
59
+ const cachedDynamicConfig = getCachedDynamicConfig();
60
+ const pathValue = process.env.VITE_BASE_PATH || cachedDynamicConfig?.vite_base_path;
61
+ if (!pathValue) {
136
62
  throw new Error("VITE_BASE_PATH must be set via environment variable or project_config.json");
137
63
  }
138
- return path;
64
+ return pathValue;
139
65
  },
140
66
  get ghPath() {
67
+ const cachedDynamicConfig = getCachedDynamicConfig();
141
68
  return process.env.GH_PATH || cachedDynamicConfig?.gh_path || "gh";
142
- }
69
+ },
143
70
  };
package/src/http.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { BoomtickMCPServer } from "./mcp/server.js";
3
+ import { initializeConfig } from "./config.js";
4
+
5
+ initializeConfig();
6
+
7
+ process.env.MCP_TRANSPORT = "http";
8
+
9
+ const server = new BoomtickMCPServer();
10
+ server.run().catch((error) => {
11
+ console.error("Fatal error running HTTP server:", error);
12
+ process.exit(1);
13
+ });
@@ -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;
@@ -5,6 +5,9 @@ import fs from "fs/promises";
5
5
  import * as healthModule from "./tools.js";
6
6
  import * as prDiffModule from "../tools/github.get_pr_diff.js";
7
7
  import * as ciLogsModule from "../tools/repo.read_ci_logs.js";
8
+ import * as prReviewsModule from "../tools/github.get_pr_reviews.js";
9
+ import * as prCommitsModule from "../tools/github.get_pr_commits.js";
10
+ import * as workflowRunsModule from "../tools/repo.get_workflow_runs.js";
8
11
 
9
12
  vi.mock("fs/promises");
10
13
  vi.mock("../config.js", () => ({
@@ -97,12 +100,16 @@ describe("BoomtickMCPServer", () => {
97
100
  });
98
101
 
99
102
  describe("ListResources", () => {
100
- it("should return resources list", async () => {
103
+ it("should return resources list containing workspace resources", async () => {
101
104
  const handler = requestHandlers.get("resources/list");
102
105
  expect(handler).toBeDefined();
103
106
  const result = await handler!({ method: "resources/list" });
104
107
  expect(result.resources).toBeDefined();
105
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");
106
113
  });
107
114
 
108
115
  it("should return resource templates list", async () => {
@@ -185,6 +192,35 @@ describe("BoomtickMCPServer", () => {
185
192
  expect(JSON.parse(result.contents[0].text)).toEqual({ status: "success" });
186
193
  });
187
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
+
188
224
  it("should throw when resource is unknown", async () => {
189
225
  const handler = requestHandlers.get("resources/read");
190
226
 
@@ -223,6 +259,31 @@ describe("BoomtickMCPServer", () => {
223
259
  });
224
260
  });
225
261
 
262
+ it("should execute github_get_pr_reviews and github_get_pr_commits via underscore or dot name", async () => {
263
+ const handler = requestHandlers.get("tools/call");
264
+ vi.spyOn(prReviewsModule, "getPrReviewsHandler").mockResolvedValue({ reviews: [] } as any);
265
+ vi.spyOn(prCommitsModule, "getPrCommitsHandler").mockResolvedValue({ commits: [] } as any);
266
+ vi.spyOn(workflowRunsModule, "getWorkflowRunsHandler").mockResolvedValue({ runs: [] } as any);
267
+
268
+ const resReviews = await handler!({
269
+ method: "tools/call",
270
+ params: { name: "github_get_pr_reviews", arguments: { prNumber: 123 } },
271
+ });
272
+ expect(resReviews.structuredContent).toEqual({ reviews: [] });
273
+
274
+ const resCommits = await handler!({
275
+ method: "tools/call",
276
+ params: { name: "github_get_pr_commits", arguments: { prNumber: 123 } },
277
+ });
278
+ expect(resCommits.structuredContent).toEqual({ commits: [] });
279
+
280
+ const resWorkflow = await handler!({
281
+ method: "tools/call",
282
+ params: { name: "repo_get_workflow_runs", arguments: {} },
283
+ });
284
+ expect(resWorkflow.structuredContent).toEqual({ runs: [] });
285
+ });
286
+
226
287
  it("should return error result for unknown tool", async () => {
227
288
  const handler = requestHandlers.get("tools/call");
228
289
 
package/src/mcp/server.ts CHANGED
@@ -24,14 +24,18 @@ import { getCommandSchemaHandler, GetCommandSchemaInputSchema } from "../tools/r
24
24
  import { getPackageScriptsHandler, GetPackageScriptsInputSchema } from "../tools/repo.get_package_scripts.js";
25
25
  import { readCiLogsHandler, ReadCiLogsInputSchema } from "../tools/repo.read_ci_logs.js";
26
26
  import { repoLogsHandler, RepoLogsInputSchema } from "../tools/repo.logs.js";
27
+ import { getWorkflowRunsHandler, GetWorkflowRunsInputSchema } from "../tools/repo.get_workflow_runs.js";
27
28
  import { createRepairBranchHandler, CreateRepairBranchInputSchema } from "../tools/repo.create_repair_branch.js";
28
29
  import { createBranchHandler, CreateBranchInputSchema } from "../tools/repo.create_branch.js";
29
30
  import { runTestsHandler, RunTestsInputSchema } from "../tools/repo.run_tests.js";
31
+ import { checkJulesStatusHandler, CheckJulesStatusInputSchema } from "../tools/repo.check_jules_status.js";
30
32
  import { commitPatchHandler, CommitPatchInputSchema } from "../tools/repo.commit_patch.js";
31
33
  import { openReplacementPrHandler, OpenReplacementPrInputSchema } from "../tools/github.open_replacement_pr.js";
32
34
  import { commentTriageSummaryHandler, CommentTriageSummaryInputSchema } from "../tools/github.comment_triage_summary.js";
33
35
  import { createPullRequestHandler, CreatePullRequestInputSchema } from "../tools/github.create_pull_request.js";
34
36
  import { getPrHandler, GetPrInputSchema } from "../tools/github.get_pr.js";
37
+ import { getPrReviewsHandler, GetPrReviewsInputSchema } from "../tools/github.get_pr_reviews.js";
38
+ import { getPrCommitsHandler, GetPrCommitsInputSchema } from "../tools/github.get_pr_commits.js";
35
39
  import { issueViewHandler, IssueViewInputSchema } from "../tools/github.issue_view.js";
36
40
  import { issueUpdateHandler, IssueUpdateInputSchema } from "../tools/github.issue_update.js";
37
41
  import { issueCommentHandler, IssueCommentInputSchema } from "../tools/github.issue_comment.js";
@@ -147,6 +151,16 @@ export class BoomtickMCPServer {
147
151
  contents: [{ uri, mimeType: "application/json", text: JSON.stringify(changedFiles, null, 2) }],
148
152
  };
149
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
+ }
150
164
  if (uri === "repo://package-json" || uri === "mcp://workspace/package-json") {
151
165
  const content = await fs.readFile(path.join(config.repoPath, "package.json"), "utf-8");
152
166
  return {
@@ -222,12 +236,18 @@ export class BoomtickMCPServer {
222
236
  return createSuccessResult(await readCiLogsHandler(ReadCiLogsInputSchema.parse(normArgs)));
223
237
  case "repo.logs":
224
238
  return createSuccessResult(await repoLogsHandler(RepoLogsInputSchema.parse(normArgs)));
239
+ case "repo.get.workflow.runs":
240
+ case "repo.get_workflow_runs":
241
+ return createSuccessResult(await getWorkflowRunsHandler(GetWorkflowRunsInputSchema.parse(normArgs)));
225
242
  case "repo.create.branch":
226
243
  case "repo.create_branch":
227
244
  return createSuccessResult(await createBranchHandler(CreateBranchInputSchema.parse(normArgs)));
228
245
  case "repo.create.repair.branch":
229
246
  case "repo.create_repair_branch":
230
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)));
231
251
  case "repo.run.tests":
232
252
  case "repo.run_tests":
233
253
  return createSuccessResult(await runTestsHandler(RunTestsInputSchema.parse(normArgs)));
@@ -246,6 +266,12 @@ export class BoomtickMCPServer {
246
266
  case "github.get.pr":
247
267
  case "github.get_pr":
248
268
  return createSuccessResult(await getPrHandler(GetPrInputSchema.parse(normArgs)));
269
+ case "github.get.pr.reviews":
270
+ case "github.get_pr_reviews":
271
+ return createSuccessResult(await getPrReviewsHandler(GetPrReviewsInputSchema.parse(normArgs)));
272
+ case "github.get.pr.commits":
273
+ case "github.get_pr_commits":
274
+ return createSuccessResult(await getPrCommitsHandler(GetPrCommitsInputSchema.parse(normArgs)));
249
275
  case "github.issue.view":
250
276
  case "github.issue_view":
251
277
  return createSuccessResult(await issueViewHandler(IssueViewInputSchema.parse(normArgs)));
@@ -371,24 +397,35 @@ export class BoomtickMCPServer {
371
397
  }
372
398
  }
373
399
 
374
- async startHttp(port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10), bearerToken?: string) {
375
- this.prepareEnvironmentAndPreflight();
376
- const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
377
- return await mcpHttp.listen(port);
378
- }
379
-
380
400
  async run() {
381
401
  this.prepareEnvironmentAndPreflight();
382
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
+
383
421
  const isHttp =
384
422
  process.env.MCP_TRANSPORT === "http" ||
385
- process.argv.includes("--http") ||
386
- (process.env.PORT !== undefined && !process.argv.includes("--stdio")) ||
387
- (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"));
388
426
 
389
427
  if (isHttp) {
390
- const port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
391
- const mcpHttp = createMcpHttpServer({ port, mcpServer: this.server });
428
+ const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
392
429
  await mcpHttp.listen(port);
393
430
  return;
394
431
  }