@arii/boomtick-mcp 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.mcp/schemas/boomtick.health.json +22 -3
  2. package/.mcp/schemas/boomtick_health.json +22 -3
  3. package/.mcp/schemas/github.checkout_branch.json +16 -3
  4. package/.mcp/schemas/github.comment_triage_summary.json +16 -3
  5. package/.mcp/schemas/github.create_pull_request.json +16 -3
  6. package/.mcp/schemas/github.get_merge_conflict_files.json +55 -5
  7. package/.mcp/schemas/github.get_pr.json +12 -3
  8. package/.mcp/schemas/github.get_pr_commits.json +12 -3
  9. package/.mcp/schemas/github.get_pr_reviews.json +24 -3
  10. package/.mcp/schemas/github.issue_comment.json +19 -3
  11. package/.mcp/schemas/github.issue_view.json +17 -3
  12. package/.mcp/schemas/github.open_replacement_pr.json +16 -3
  13. package/.mcp/schemas/github_checkout_branch.json +16 -3
  14. package/.mcp/schemas/github_comment_triage_summary.json +16 -3
  15. package/.mcp/schemas/github_create_pull_request.json +16 -3
  16. package/.mcp/schemas/github_get_merge_conflict_files.json +55 -5
  17. package/.mcp/schemas/github_get_pr.json +12 -3
  18. package/.mcp/schemas/github_get_pr_commits.json +12 -3
  19. package/.mcp/schemas/github_get_pr_reviews.json +24 -3
  20. package/.mcp/schemas/github_issue_comment.json +19 -3
  21. package/.mcp/schemas/github_issue_view.json +17 -3
  22. package/.mcp/schemas/github_open_replacement_pr.json +16 -3
  23. package/.mcp/schemas/jules.cancel_session.json +38 -3
  24. package/.mcp/schemas/jules.create_session.json +52 -3
  25. package/.mcp/schemas/jules.get_messages.json +19 -3
  26. package/.mcp/schemas/jules.get_pr.json +23 -3
  27. package/.mcp/schemas/jules.get_session.json +38 -3
  28. package/.mcp/schemas/jules.list_sessions.json +53 -3
  29. package/.mcp/schemas/jules.send_message.json +31 -3
  30. package/.mcp/schemas/jules.trigger_feedback.json +28 -3
  31. package/.mcp/schemas/jules_cancel_session.json +38 -3
  32. package/.mcp/schemas/jules_create_session.json +52 -3
  33. package/.mcp/schemas/jules_get_messages.json +19 -3
  34. package/.mcp/schemas/jules_get_pr.json +23 -3
  35. package/.mcp/schemas/jules_get_session.json +38 -3
  36. package/.mcp/schemas/jules_list_sessions.json +53 -3
  37. package/.mcp/schemas/jules_send_message.json +31 -3
  38. package/.mcp/schemas/jules_trigger_feedback.json +28 -3
  39. package/.mcp/schemas/repo.check_jules_status.json +93 -3
  40. package/.mcp/schemas/repo.commit_patch.json +23 -3
  41. package/.mcp/schemas/repo.create_branch.json +91 -3
  42. package/.mcp/schemas/repo.create_repair_branch.json +94 -3
  43. package/.mcp/schemas/repo.get_changed_files.json +50 -3
  44. package/.mcp/schemas/repo.get_command_schema.json +29 -3
  45. package/.mcp/schemas/repo.get_package_scripts.json +11 -3
  46. package/.mcp/schemas/repo.get_workflow_runs.json +12 -3
  47. package/.mcp/schemas/repo.logs.json +14 -3
  48. package/.mcp/schemas/repo.multi_exec.json +89 -0
  49. package/.mcp/schemas/repo.read_agent_context.json +117 -5
  50. package/.mcp/schemas/repo.read_ci_logs.json +46 -3
  51. package/.mcp/schemas/repo_check_jules_status.json +93 -3
  52. package/.mcp/schemas/repo_commit_patch.json +23 -3
  53. package/.mcp/schemas/repo_create_branch.json +91 -3
  54. package/.mcp/schemas/repo_create_repair_branch.json +94 -3
  55. package/.mcp/schemas/repo_get_changed_files.json +50 -3
  56. package/.mcp/schemas/repo_get_command_schema.json +29 -3
  57. package/.mcp/schemas/repo_get_package_scripts.json +11 -3
  58. package/.mcp/schemas/repo_get_workflow_runs.json +12 -3
  59. package/.mcp/schemas/repo_logs.json +14 -3
  60. package/.mcp/schemas/repo_multi_exec.json +89 -0
  61. package/.mcp/schemas/repo_read_agent_context.json +117 -5
  62. package/.mcp/schemas/repo_read_ci_logs.json +46 -3
  63. package/CHANGELOG.md +22 -0
  64. package/README.md +7 -5
  65. package/dist/config/dynamic.js +3 -2
  66. package/dist/config/repo.js +5 -5
  67. package/dist/config.test.js +10 -3
  68. package/dist/lib/git.js +78 -6
  69. package/dist/lib/git.test.js +34 -1
  70. package/dist/mcp/definitions.js +49 -36
  71. package/dist/mcp/http.js +15 -0
  72. package/dist/mcp/http.test.js +8 -0
  73. package/dist/mcp/server.js +34 -0
  74. package/dist/mcp/server.test.js +31 -0
  75. package/dist/tools/contract.js +1264 -92
  76. package/dist/tools/github.get_merge_conflict_files.js +10 -2
  77. package/dist/tools/jules/create-session.js +12 -2
  78. package/dist/tools/jules/create-session.test.js +7 -0
  79. package/dist/tools/repo.create_branch.js +18 -4
  80. package/dist/tools/repo.create_branch.test.js +41 -5
  81. package/dist/tools/repo.create_repair_branch.js +26 -9
  82. package/dist/tools/repo.get_changed_files.js +10 -2
  83. package/dist/tools/repo.multi_exec.js +21 -0
  84. package/dist/tools/repo.multi_exec.test.js +13 -0
  85. package/dist/tools/repo.read_agent_context.js +65 -3
  86. package/dist/tools/repo.read_agent_context.test.js +30 -1
  87. package/package.json +1 -1
  88. package/src/config/dynamic.ts +3 -2
  89. package/src/config/repo.ts +6 -6
  90. package/src/config.test.ts +11 -3
  91. package/src/lib/git.test.ts +45 -1
  92. package/src/lib/git.ts +94 -6
  93. package/src/mcp/definitions.ts +77 -36
  94. package/src/mcp/http.test.ts +10 -0
  95. package/src/mcp/http.ts +20 -0
  96. package/src/mcp/server.test.ts +38 -0
  97. package/src/mcp/server.ts +36 -0
  98. package/src/tools/contract.ts +1355 -67
  99. package/src/tools/github.get_merge_conflict_files.ts +11 -2
  100. package/src/tools/jules/create-session.test.ts +9 -0
  101. package/src/tools/jules/create-session.ts +13 -2
  102. package/src/tools/repo.create_branch.test.ts +73 -5
  103. package/src/tools/repo.create_branch.ts +30 -4
  104. package/src/tools/repo.create_repair_branch.ts +31 -9
  105. package/src/tools/repo.get_changed_files.ts +10 -2
  106. package/src/tools/repo.multi_exec.test.ts +15 -0
  107. package/src/tools/repo.multi_exec.ts +23 -0
  108. package/src/tools/repo.read_agent_context.test.ts +35 -1
  109. package/src/tools/repo.read_agent_context.ts +68 -3
package/src/lib/git.ts CHANGED
@@ -2,6 +2,7 @@ import { runCommand } from "./shell.js";
2
2
  import { config } from "../config.js";
3
3
  import path from "path";
4
4
  import fs from "fs/promises";
5
+ import * as syncFs from "fs";
5
6
  import { execSync } from "child_process";
6
7
 
7
8
  const repoCache = new Map<string, string>();
@@ -37,6 +38,87 @@ export function resolveRepo(explicitRepo?: string | null, cwd?: string): string
37
38
  return fallback;
38
39
  }
39
40
 
41
+ export interface RepoPathOptions {
42
+ path?: string | null;
43
+ repoPath?: string | null;
44
+ repo?: string | null;
45
+ cwd?: string | null;
46
+ }
47
+
48
+ export function resolveRepoPath(options: RepoPathOptions = {}): string {
49
+ // 1. Explicit path parameter has highest precedence
50
+ if (options.path && options.path.trim().length > 0) {
51
+ return path.resolve(options.path.trim());
52
+ }
53
+
54
+ // 2. repoPath alias parameter
55
+ if (options.repoPath && options.repoPath.trim().length > 0) {
56
+ return path.resolve(options.repoPath.trim());
57
+ }
58
+
59
+ // 3. Repository name override (e.g. "arii/tech-dancer" or "tech-dancer")
60
+ if (options.repo && options.repo.trim().length > 0) {
61
+ const rawRepo = options.repo.trim();
62
+ const repoName = rawRepo.includes("/") ? rawRepo.split("/")[1] : rawRepo;
63
+
64
+ // First check project_config.json ecosystem_repos if available
65
+ let rootPath: string = process.cwd();
66
+ try {
67
+ if (options.cwd && options.cwd.trim().length > 0) {
68
+ rootPath = options.cwd.trim();
69
+ } else if (config.repoPath) {
70
+ rootPath = config.repoPath;
71
+ }
72
+ } catch {
73
+ rootPath = process.cwd();
74
+ }
75
+
76
+ const projectConfigPath = path.join(rootPath, "project_config.json");
77
+ if (syncFs.existsSync(projectConfigPath)) {
78
+ try {
79
+ const parsed = JSON.parse(syncFs.readFileSync(projectConfigPath, "utf-8"));
80
+ if (parsed?.ecosystem_repos && typeof parsed.ecosystem_repos === "object") {
81
+ for (const [key, relPath] of Object.entries(parsed.ecosystem_repos)) {
82
+ const keyRepoName = key.includes("/") ? key.split("/")[1] : key;
83
+ if (key === rawRepo || keyRepoName === repoName) {
84
+ return path.resolve(rootPath, relPath as string);
85
+ }
86
+ }
87
+ }
88
+ } catch {
89
+ // Fallback to directory inspection
90
+ }
91
+ }
92
+
93
+ // Check sibling directory relative to cwd or rootPath
94
+ const candidateDirs = [
95
+ path.resolve(process.cwd(), "..", repoName),
96
+ path.resolve(rootPath, "..", repoName),
97
+ ];
98
+ for (const cand of candidateDirs) {
99
+ if (syncFs.existsSync(cand)) {
100
+ return cand;
101
+ }
102
+ }
103
+ }
104
+
105
+ // 4. Explicit cwd parameter
106
+ if (options.cwd && options.cwd.trim().length > 0) {
107
+ return path.resolve(options.cwd.trim());
108
+ }
109
+
110
+ // 5. Configured repoPath or process.cwd()
111
+ try {
112
+ if (config.repoPath && syncFs.existsSync(config.repoPath)) {
113
+ return config.repoPath;
114
+ }
115
+ } catch {
116
+ // Fall back to process.cwd()
117
+ }
118
+
119
+ return process.cwd();
120
+ }
121
+
40
122
  function validatePrNumber(value: unknown): number {
41
123
  const prNumber = Number(value);
42
124
 
@@ -47,20 +129,25 @@ function validatePrNumber(value: unknown): number {
47
129
  return prNumber;
48
130
  }
49
131
 
50
- export async function createWorktree(branch: string, prNumber: number): Promise<string> {
132
+ export async function createWorktree(branch: string, prNumber: number, baseRepoPath?: string): Promise<string> {
51
133
  const safePrNumber = validatePrNumber(prNumber);
52
134
 
53
135
  const workspaceRoot = "/tmp/boomtick-worktrees";
54
136
  // nosemgrep
55
137
  const worktreePath = path.join(workspaceRoot, `boomtick-mcp-rescue-${safePrNumber}`);
138
+ const cwdOption = baseRepoPath ? { cwd: baseRepoPath } : undefined;
56
139
 
57
140
  // Clean up if exists
58
141
  try {
59
142
  await fs.rm(worktreePath, { recursive: true, force: true });
60
- await runCommand("git", ["worktree", "prune"]);
143
+ await runCommand("git", ["worktree", "prune"], cwdOption);
61
144
  } catch (e) { /* ignore if no previous worktree */ }
62
145
 
63
- const result = await runCommand("git", ["worktree", "add", "-b", `repair-pr-${prNumber}-${Date.now()}`, worktreePath, branch]);
146
+ const result = await runCommand(
147
+ "git",
148
+ ["worktree", "add", "-b", `repair-pr-${prNumber}-${Date.now()}`, worktreePath, branch],
149
+ cwdOption
150
+ );
64
151
 
65
152
  if (result.exitCode !== 0) {
66
153
  throw new Error(`Failed to create worktree: ${result.stderr}`);
@@ -69,9 +156,10 @@ export async function createWorktree(branch: string, prNumber: number): Promise<
69
156
  return worktreePath;
70
157
  }
71
158
 
72
- export async function removeWorktree(worktreePath: string): Promise<void> {
73
- await runCommand("git", ["worktree", "remove", "--force", worktreePath]);
74
- await runCommand("git", ["worktree", "prune"]);
159
+ export async function removeWorktree(worktreePath: string, baseRepoPath?: string): Promise<void> {
160
+ const cwdOption = baseRepoPath ? { cwd: baseRepoPath } : undefined;
161
+ await runCommand("git", ["worktree", "remove", "--force", worktreePath], cwdOption);
162
+ await runCommand("git", ["worktree", "prune"], cwdOption);
75
163
  }
76
164
 
77
165
  export interface ChangedFile {
@@ -1,41 +1,70 @@
1
1
  import { Tool, Prompt, Resource, ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
2
2
  import {
3
3
  HealthCheckInputJsonSchema,
4
+ HealthCheckResponseJsonSchema,
4
5
  SearchPRsInputJsonSchema,
5
6
  SearchPRsResponseJsonSchema,
6
7
  GetPrDiffInputJsonSchema,
7
8
  PrDiffResponseJsonSchema,
8
9
  CheckoutBranchInputJsonSchema,
10
+ CheckoutBranchResponseJsonSchema,
9
11
  GetMergeConflictFilesInputJsonSchema,
10
12
  GetMergeConflictFilesResponseJsonSchema,
11
13
  GetChangedFilesInputJsonSchema,
14
+ GetChangedFilesResponseJsonSchema,
12
15
  GetPackageScriptsInputJsonSchema,
16
+ GetPackageScriptsResponseJsonSchema,
13
17
  ReadCiLogsInputJsonSchema,
18
+ ReadCiLogsResponseJsonSchema,
14
19
  RepoLogsInputJsonSchema,
20
+ RepoLogsResponseJsonSchema,
15
21
  CreateBranchInputJsonSchema,
22
+ CreateBranchResponseJsonSchema,
16
23
  CreateRepairBranchInputJsonSchema,
24
+ CreateRepairBranchResponseJsonSchema,
25
+ CheckJulesStatusResponseJsonSchema,
17
26
  RunTestsInputJsonSchema,
18
27
  RunTestsResponseJsonSchema,
19
28
  CommitPatchInputJsonSchema,
29
+ CommitPatchResponseJsonSchema,
20
30
  OpenReplacementPrInputJsonSchema,
31
+ OpenReplacementPrResponseJsonSchema,
21
32
  CreatePullRequestInputJsonSchema,
33
+ CreatePullRequestResponseJsonSchema,
22
34
  CommentTriageSummaryInputJsonSchema,
35
+ CommentTriageSummaryResponseJsonSchema,
23
36
  GetPrInputJsonSchema,
37
+ GetPrResponseJsonSchema,
24
38
  IssueViewInputJsonSchema,
39
+ IssueViewResponseJsonSchema,
25
40
  IssueUpdateInputJsonSchema,
26
41
  IssueUpdateResponseJsonSchema,
27
42
  IssueCommentInputJsonSchema,
43
+ IssueCommentResponseJsonSchema,
28
44
  CreateIssueInputJsonSchema,
29
45
  CreateIssueResponseJsonSchema,
30
46
  GetCommandSchemaInputJsonSchema,
47
+ GetCommandSchemaResponseJsonSchema,
31
48
  ReadAgentContextInputJsonSchema as ReadAgentContextSchema,
49
+ ReadAgentContextResponseJsonSchema,
50
+ MultiExecInputJsonSchema,
51
+ MultiExecResponseJsonSchema,
32
52
  CreateJulesSessionInputJsonSchema,
33
53
  JulesSessionIdInputJsonSchema,
34
54
  JulesSendMessageInputJsonSchema,
55
+ JulesSendMessageResponseJsonSchema,
56
+ JulesGetMessagesResponseJsonSchema,
35
57
  JulesListSessionsInputJsonSchema,
58
+ JulesListSessionsResponseJsonSchema,
59
+ JulesSessionResponseJsonSchema,
60
+ JulesGetPrResponseJsonSchema,
61
+ JulesTriggerFeedbackResponseJsonSchema,
36
62
  GetPrReviewsInputJsonSchema,
63
+ GetPrReviewsResponseJsonSchema,
37
64
  GetPrCommitsInputJsonSchema,
65
+ GetPrCommitsResponseJsonSchema,
38
66
  GetWorkflowRunsInputJsonSchema,
67
+ GetWorkflowRunsResponseJsonSchema,
39
68
  } from "../tools/contract.js";
40
69
 
41
70
  export const MCP_PROMPTS: Prompt[] = [
@@ -78,12 +107,24 @@ export const MCP_RESOURCES: Resource[] = [
78
107
  mimeType: "text/markdown",
79
108
  description: "The agent contract specification file (.agents/AGENT_CONTRACT.md).",
80
109
  },
110
+ {
111
+ uri: "mcp://workspace/project-config",
112
+ name: "Project Configuration",
113
+ mimeType: "application/json",
114
+ description: "The project configuration (project_config.json).",
115
+ },
81
116
  {
82
117
  uri: "mcp://git/changed-files",
83
118
  name: "Git Changed Files",
84
119
  mimeType: "application/json",
85
120
  description: "List of changed files in the repository.",
86
121
  },
122
+ {
123
+ uri: "mcp://git/status",
124
+ name: "Git Status",
125
+ mimeType: "application/json",
126
+ description: "Current working directory git status.",
127
+ },
87
128
  {
88
129
  uri: "mcp://ci/latest-run",
89
130
  name: "Latest CI Run",
@@ -125,18 +166,12 @@ export const MCP_RESOURCE_TEMPLATES: ResourceTemplate[] = [
125
166
  },
126
167
  ];
127
168
 
128
- const DEFAULT_OUTPUT_SCHEMA = {
129
- type: "object",
130
- properties: {},
131
- additionalProperties: true,
132
- } as const;
133
-
134
169
  const BASE_MCP_TOOLS: Tool[] = [
135
170
  {
136
171
  name: "boomtick.health",
137
172
  description: "Check the health and configuration of the MCP server.",
138
173
  inputSchema: HealthCheckInputJsonSchema as any,
139
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
174
+ outputSchema: HealthCheckResponseJsonSchema as any,
140
175
  },
141
176
  {
142
177
  name: "github.search_open_prs",
@@ -146,9 +181,15 @@ const BASE_MCP_TOOLS: Tool[] = [
146
181
  },
147
182
  {
148
183
  name: "repo.read_agent_context",
149
- description: "Read the full repository index including file tree, CLI schema, and MCP tools.",
184
+ description: "Read the repository index including metadata, model review chains, or multi-repo ecosystem status.",
150
185
  inputSchema: ReadAgentContextSchema as any,
151
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
186
+ outputSchema: ReadAgentContextResponseJsonSchema as any,
187
+ },
188
+ {
189
+ name: "repo.multi_exec",
190
+ description: "Execute a shell command sequentially across ecosystem repositories (or a filtered subset).",
191
+ inputSchema: MultiExecInputJsonSchema as any,
192
+ outputSchema: MultiExecResponseJsonSchema as any,
152
193
  },
153
194
  {
154
195
  name: "github.get_pr_diff",
@@ -160,7 +201,7 @@ const BASE_MCP_TOOLS: Tool[] = [
160
201
  name: "github.checkout_branch",
161
202
  description: "Checkout a specific branch in the repository or worktree.",
162
203
  inputSchema: CheckoutBranchInputJsonSchema as any,
163
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
204
+ outputSchema: CheckoutBranchResponseJsonSchema as any,
164
205
  },
165
206
  {
166
207
  name: "github.get_merge_conflict_files",
@@ -172,43 +213,43 @@ const BASE_MCP_TOOLS: Tool[] = [
172
213
  name: "repo.get_changed_files",
173
214
  description: "Get the list of changed files between two refs.",
174
215
  inputSchema: GetChangedFilesInputJsonSchema as any,
175
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
216
+ outputSchema: GetChangedFilesResponseJsonSchema as any,
176
217
  },
177
218
  {
178
219
  name: "repo.get_command_schema",
179
220
  description: "Retrieve a lightweight, targeted schema for a specific td subcommand. Use this instead of reading the entire cli-schema.json to save tokens.",
180
221
  inputSchema: GetCommandSchemaInputJsonSchema as any,
181
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
222
+ outputSchema: GetCommandSchemaResponseJsonSchema as any,
182
223
  },
183
224
  {
184
225
  name: "repo.get_package_scripts",
185
226
  description: "Get the scripts defined in package.json.",
186
227
  inputSchema: GetPackageScriptsInputJsonSchema as any,
187
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
228
+ outputSchema: GetPackageScriptsResponseJsonSchema as any,
188
229
  },
189
230
  {
190
231
  name: "repo.read_ci_logs",
191
232
  description: "Read CI logs for a given pull request.",
192
233
  inputSchema: ReadCiLogsInputJsonSchema as any,
193
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
234
+ outputSchema: ReadCiLogsResponseJsonSchema as any,
194
235
  },
195
236
  {
196
237
  name: "repo.logs",
197
238
  description: "Stream or grep combined CI logs for all jobs in a pull request.",
198
239
  inputSchema: RepoLogsInputJsonSchema as any,
199
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
240
+ outputSchema: RepoLogsResponseJsonSchema as any,
200
241
  },
201
242
  {
202
243
  name: "repo.create_branch",
203
244
  description: "Creates a new clean git branch from a target base branch.",
204
245
  inputSchema: CreateBranchInputJsonSchema as any,
205
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
246
+ outputSchema: CreateBranchResponseJsonSchema as any,
206
247
  },
207
248
  {
208
249
  name: "repo.create_repair_branch",
209
250
  description: "Creates a fresh worktree and a new repair branch for an existing PR. This isolates the repair work from the main codebase.",
210
251
  inputSchema: CreateRepairBranchInputJsonSchema as any,
211
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
252
+ outputSchema: CreateRepairBranchResponseJsonSchema as any,
212
253
  },
213
254
  {
214
255
  name: "repo.check_jules_status",
@@ -224,7 +265,7 @@ const BASE_MCP_TOOLS: Tool[] = [
224
265
  intervalMs: { type: "integer", description: "Polling interval in milliseconds." },
225
266
  },
226
267
  } as any,
227
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
268
+ outputSchema: CheckJulesStatusResponseJsonSchema as any,
228
269
  },
229
270
  {
230
271
  name: "repo.run_tests",
@@ -236,55 +277,55 @@ const BASE_MCP_TOOLS: Tool[] = [
236
277
  name: "repo.commit_patch",
237
278
  description: "Commit verified repair changes in a worktree.",
238
279
  inputSchema: CommitPatchInputJsonSchema as any,
239
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
280
+ outputSchema: CommitPatchResponseJsonSchema as any,
240
281
  },
241
282
  {
242
283
  name: "github.open_replacement_pr",
243
284
  description: "Open a new PR that replaces or repairs the original PR.",
244
285
  inputSchema: OpenReplacementPrInputJsonSchema as any,
245
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
286
+ outputSchema: OpenReplacementPrResponseJsonSchema as any,
246
287
  },
247
288
  {
248
289
  name: "github.create_pull_request",
249
290
  description: "Creates a pull request on GitHub using the MCP server's integrated credentials. Bypasses terminal CLI constraints.",
250
291
  inputSchema: CreatePullRequestInputJsonSchema as any,
251
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
292
+ outputSchema: CreatePullRequestResponseJsonSchema as any,
252
293
  },
253
294
  {
254
295
  name: "github.comment_triage_summary",
255
296
  description: "Comment on the original PR with a diagnosis and replacement link.",
256
297
  inputSchema: CommentTriageSummaryInputJsonSchema as any,
257
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
298
+ outputSchema: CommentTriageSummaryResponseJsonSchema as any,
258
299
  },
259
300
  {
260
301
  name: "github.get_pr",
261
302
  description: "Get details of a GitHub PR including title, state, and URLs.",
262
303
  inputSchema: GetPrInputJsonSchema as any,
263
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
304
+ outputSchema: GetPrResponseJsonSchema as any,
264
305
  },
265
306
  {
266
307
  name: "github.get_pr_reviews",
267
308
  description: "Get top-level reviews, verdicts, states, and bodies for a GitHub PR.",
268
309
  inputSchema: GetPrReviewsInputJsonSchema as any,
269
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
310
+ outputSchema: GetPrReviewsResponseJsonSchema as any,
270
311
  },
271
312
  {
272
313
  name: "github.get_pr_commits",
273
314
  description: "List commits and commit metadata (SHAs, authors, messages) for a GitHub PR.",
274
315
  inputSchema: GetPrCommitsInputJsonSchema as any,
275
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
316
+ outputSchema: GetPrCommitsResponseJsonSchema as any,
276
317
  },
277
318
  {
278
319
  name: "repo.get_workflow_runs",
279
320
  description: "List recent GitHub Actions workflow runs for the repository.",
280
321
  inputSchema: GetWorkflowRunsInputJsonSchema as any,
281
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
322
+ outputSchema: GetWorkflowRunsResponseJsonSchema as any,
282
323
  },
283
324
  {
284
325
  name: "github.issue_view",
285
326
  description: "View details of a GitHub issue including title, body, and state.",
286
327
  inputSchema: IssueViewInputJsonSchema as any,
287
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
328
+ outputSchema: IssueViewResponseJsonSchema as any,
288
329
  },
289
330
  {
290
331
  name: "github.issue_update",
@@ -296,7 +337,7 @@ const BASE_MCP_TOOLS: Tool[] = [
296
337
  name: "github.issue_comment",
297
338
  description: "Add a new comment to a GitHub issue.",
298
339
  inputSchema: IssueCommentInputJsonSchema as any,
299
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
340
+ outputSchema: IssueCommentResponseJsonSchema as any,
300
341
  },
301
342
  {
302
343
  name: "github.create_issue",
@@ -308,49 +349,49 @@ const BASE_MCP_TOOLS: Tool[] = [
308
349
  name: "jules.create_session",
309
350
  description: "Create a Jules session that performs work externally and may generate a GitHub pull request.",
310
351
  inputSchema: CreateJulesSessionInputJsonSchema as any,
311
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
352
+ outputSchema: JulesSessionResponseJsonSchema as any,
312
353
  },
313
354
  {
314
355
  name: "jules.get_session",
315
356
  description: "Get the status and details of a Jules session.",
316
357
  inputSchema: JulesSessionIdInputJsonSchema as any,
317
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
358
+ outputSchema: JulesSessionResponseJsonSchema as any,
318
359
  },
319
360
  {
320
361
  name: "jules.send_message",
321
362
  description: "Send a message to an active Jules session.",
322
363
  inputSchema: JulesSendMessageInputJsonSchema as any,
323
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
364
+ outputSchema: JulesSendMessageResponseJsonSchema as any,
324
365
  },
325
366
  {
326
367
  name: "jules.get_messages",
327
368
  description: "Get the message history of a Jules session.",
328
369
  inputSchema: JulesSessionIdInputJsonSchema as any,
329
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
370
+ outputSchema: JulesGetMessagesResponseJsonSchema as any,
330
371
  },
331
372
  {
332
373
  name: "jules.list_sessions",
333
374
  description: "List all Jules sessions.",
334
375
  inputSchema: JulesListSessionsInputJsonSchema as any,
335
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
376
+ outputSchema: JulesListSessionsResponseJsonSchema as any,
336
377
  },
337
378
  {
338
379
  name: "jules.cancel_session",
339
380
  description: "Cancel an ongoing Jules session.",
340
381
  inputSchema: JulesSessionIdInputJsonSchema as any,
341
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
382
+ outputSchema: JulesSessionResponseJsonSchema as any,
342
383
  },
343
384
  {
344
385
  name: "jules.get_pr",
345
386
  description: "Get the generated pull request url associated with an active Jules agent session.",
346
387
  inputSchema: JulesSessionIdInputJsonSchema as any,
347
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
388
+ outputSchema: JulesGetPrResponseJsonSchema as any,
348
389
  },
349
390
  {
350
391
  name: "jules.trigger_feedback",
351
392
  description: "Automatically collect CI status/logs for the PR associated with a Jules session and send them back as feedback.",
352
393
  inputSchema: JulesSessionIdInputJsonSchema as any,
353
- outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
394
+ outputSchema: JulesTriggerFeedbackResponseJsonSchema as any,
354
395
  },
355
396
  ];
356
397
 
@@ -18,6 +18,16 @@ describe("createMcpHttpServer", () => {
18
18
  }
19
19
  });
20
20
 
21
+ it("should respond with 200 on GET /health endpoint", async () => {
22
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
23
+ port = await httpServerObj.listen(0);
24
+
25
+ const res = await fetch(`http://localhost:${port}/health`);
26
+ expect(res.status).toBe(200);
27
+ const body = await res.json();
28
+ expect(body).toEqual({ status: "ok", transport: "http" });
29
+ });
30
+
21
31
  it("should respond with RFC 9728 OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
22
32
  httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
23
33
  port = await httpServerObj.listen(0);
package/src/mcp/http.ts CHANGED
@@ -32,6 +32,13 @@ export function createMcpHttpServer(options: HttpServerOptions) {
32
32
 
33
33
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
34
34
 
35
+ // Endpoint 0: Health check endpoint for readiness probes / load balancers
36
+ if (url.pathname === "/health" && req.method === "GET") {
37
+ res.writeHead(200, { "Content-Type": "application/json" });
38
+ res.end(JSON.stringify({ status: "ok", transport: "http" }));
39
+ return;
40
+ }
41
+
35
42
  // Endpoint 1: RFC 9728 OAuth Protected Resource Metadata Discovery
36
43
  if (url.pathname === "/.well-known/oauth-protected-resource" && (req.method === "GET" || req.method === "HEAD")) {
37
44
  const metadata = {
@@ -82,11 +89,24 @@ export function createMcpHttpServer(options: HttpServerOptions) {
82
89
 
83
90
  // Endpoint 2: SSE stream setup
84
91
  if (url.pathname === "/sse" && req.method === "GET") {
92
+ res.setHeader("Cache-Control", "no-cache");
93
+ res.setHeader("Content-Type", "text/event-stream");
94
+ res.setHeader("Connection", "keep-alive");
95
+
85
96
  const transport = new SSEServerTransport("/messages", res);
86
97
  transports.set(transport.sessionId, transport);
98
+
99
+ const keepAliveInterval = setInterval(() => {
100
+ if (!res.writableEnded) {
101
+ res.write(": keep-alive\n\n");
102
+ }
103
+ }, 15000);
104
+
87
105
  transport.onclose = () => {
106
+ clearInterval(keepAliveInterval);
88
107
  transports.delete(transport.sessionId);
89
108
  };
109
+
90
110
  await mcpServer.connect(transport);
91
111
  return;
92
112
  }
@@ -207,6 +207,33 @@ describe("BoomtickMCPServer", () => {
207
207
  expect(JSON.parse(result.contents[0].text)).toEqual({ files: ["src/index.ts"] });
208
208
  });
209
209
 
210
+ it("should handle mcp://workspace/project-config", async () => {
211
+ const handler = requestHandlers.get("resources/read");
212
+ vi.mocked(fs.readFile).mockResolvedValue('{"packageName": "boomtick"}');
213
+
214
+ const result = await handler!({
215
+ method: "resources/read",
216
+ params: { uri: "mcp://workspace/project-config" },
217
+ });
218
+
219
+ expect(result.contents[0].uri).toBe("mcp://workspace/project-config");
220
+ expect(result.contents[0].mimeType).toBe("application/json");
221
+ expect(JSON.parse(result.contents[0].text)).toEqual({ packageName: "boomtick" });
222
+ });
223
+
224
+ it("should handle mcp://git/status", async () => {
225
+ const handler = requestHandlers.get("resources/read");
226
+
227
+ const result = await handler!({
228
+ method: "resources/read",
229
+ params: { uri: "mcp://git/status" },
230
+ });
231
+
232
+ expect(result.contents[0].uri).toBe("mcp://git/status");
233
+ expect(result.contents[0].mimeType).toBe("application/json");
234
+ expect(JSON.parse(result.contents[0].text)).toHaveProperty("status");
235
+ });
236
+
210
237
  it("should handle mcp://ci/latest-run", async () => {
211
238
  const handler = requestHandlers.get("resources/read");
212
239
  vi.spyOn(workflowRunsModule, "getWorkflowRunsHandler").mockResolvedValue({ workflowRuns: [{ id: 1, status: "completed", conclusion: "success" }] } as any);
@@ -241,6 +268,17 @@ describe("BoomtickMCPServer", () => {
241
268
  expect(result.tools).toBeDefined();
242
269
  expect(Array.isArray(result.tools)).toBe(true);
243
270
  });
271
+
272
+ it("should ensure all tools have typed outputSchema and none are empty or untyped DEFAULT_OUTPUT_SCHEMA", async () => {
273
+ const handler = requestHandlers.get("tools/list");
274
+ const result = await handler!({ method: "tools/list" });
275
+ for (const tool of result.tools) {
276
+ expect(tool.outputSchema).toBeDefined();
277
+ expect(tool.outputSchema.type).toBe("object");
278
+ expect(tool.outputSchema.properties).toBeDefined();
279
+ expect(Object.keys(tool.outputSchema.properties).length).toBeGreaterThan(0);
280
+ }
281
+ });
244
282
  });
245
283
 
246
284
  describe("CallTool", () => {
package/src/mcp/server.ts CHANGED
@@ -20,6 +20,7 @@ import { getMergeConflictFilesHandler, GetMergeConflictFilesInputSchema } from "
20
20
  import { checkoutBranchHandler, CheckoutBranchInputSchema } from "../tools/github.checkout_branch.js";
21
21
  import { getChangedFilesHandler, GetChangedFilesInputSchema } from "../tools/repo.get_changed_files.js";
22
22
  import { readAgentContextHandler, ReadAgentContextInputSchema } from "../tools/repo.read_agent_context.js";
23
+ import { multiExecHandler, MultiExecInputSchema } from "../tools/repo.multi_exec.js";
23
24
  import { getCommandSchemaHandler, GetCommandSchemaInputSchema } from "../tools/repo.get_command_schema.js";
24
25
  import { getPackageScriptsHandler, GetPackageScriptsInputSchema } from "../tools/repo.get_package_scripts.js";
25
26
  import { readCiLogsHandler, ReadCiLogsInputSchema } from "../tools/repo.read_ci_logs.js";
@@ -142,6 +143,22 @@ export class BoomtickMCPServer {
142
143
  contents: [{ uri, mimeType: "text/markdown", text: content }],
143
144
  };
144
145
  }
146
+ if (
147
+ uri === "mcp://workspace/project-config" ||
148
+ uri === "mcp://workspace/project_config" ||
149
+ uri === "repo://project-config"
150
+ ) {
151
+ const configPath = path.join(config.repoPath, "project_config.json");
152
+ let content = "{}";
153
+ try {
154
+ content = await fs.readFile(configPath, "utf-8");
155
+ } catch {
156
+ // Fallback if file doesn't exist
157
+ }
158
+ return {
159
+ contents: [{ uri, mimeType: "application/json", text: content }],
160
+ };
161
+ }
145
162
  if (
146
163
  uri === "mcp://git/changed-files" ||
147
164
  uri === "repo://changed-files"
@@ -151,6 +168,22 @@ export class BoomtickMCPServer {
151
168
  contents: [{ uri, mimeType: "application/json", text: JSON.stringify(changedFiles, null, 2) }],
152
169
  };
153
170
  }
171
+ if (
172
+ uri === "mcp://git/status" ||
173
+ uri === "repo://git-status" ||
174
+ uri === "repo://status"
175
+ ) {
176
+ let statusText = "";
177
+ try {
178
+ const res = spawnSync("git", ["status", "--porcelain"], { cwd: config.repoPath, encoding: "utf-8" });
179
+ statusText = res.stdout || "";
180
+ } catch {
181
+ // Fallback if git fails
182
+ }
183
+ return {
184
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ status: statusText.trim() }, null, 2) }],
185
+ };
186
+ }
154
187
  if (
155
188
  uri === "mcp://ci/latest-run" ||
156
189
  uri === "repo://ci/latest-run" ||
@@ -213,6 +246,9 @@ export class BoomtickMCPServer {
213
246
  case "repo.read.agent.context":
214
247
  case "repo.read_agent_context":
215
248
  return createSuccessResult(await readAgentContextHandler(ReadAgentContextInputSchema.parse(normArgs)));
249
+ case "repo.multi.exec":
250
+ case "repo.multi_exec":
251
+ return createSuccessResult(await multiExecHandler(MultiExecInputSchema.parse(normArgs)));
216
252
  case "github.get.pr.diff":
217
253
  case "github.get_pr_diff":
218
254
  return createSuccessResult(await getPrDiffHandler(GetPrDiffInputSchema.parse(normArgs)));