@arii/boomtick-mcp 0.5.0 → 0.6.0

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 (156) hide show
  1. package/.mcp/schemas/boomtick.health.json +5 -0
  2. package/.mcp/schemas/boomtick_health.json +5 -0
  3. package/.mcp/schemas/github.checkout_branch.json +5 -0
  4. package/.mcp/schemas/github.comment_triage_summary.json +5 -0
  5. package/.mcp/schemas/github.create_issue.json +5 -0
  6. package/.mcp/schemas/github.create_pull_request.json +5 -0
  7. package/.mcp/schemas/github.get_merge_conflict_files.json +5 -0
  8. package/.mcp/schemas/github.get_pr.json +5 -0
  9. package/.mcp/schemas/github.get_pr_commits.json +5 -0
  10. package/.mcp/schemas/github.get_pr_diff.json +5 -0
  11. package/.mcp/schemas/github.get_pr_reviews.json +5 -0
  12. package/.mcp/schemas/github.issue_comment.json +5 -0
  13. package/.mcp/schemas/github.issue_update.json +5 -0
  14. package/.mcp/schemas/github.issue_view.json +5 -0
  15. package/.mcp/schemas/github.open_replacement_pr.json +5 -0
  16. package/.mcp/schemas/github.search_open_prs.json +5 -0
  17. package/.mcp/schemas/github_checkout_branch.json +5 -0
  18. package/.mcp/schemas/github_comment_triage_summary.json +5 -0
  19. package/.mcp/schemas/github_create_issue.json +5 -0
  20. package/.mcp/schemas/github_create_pull_request.json +5 -0
  21. package/.mcp/schemas/github_get_merge_conflict_files.json +5 -0
  22. package/.mcp/schemas/github_get_pr.json +5 -0
  23. package/.mcp/schemas/github_get_pr_commits.json +5 -0
  24. package/.mcp/schemas/github_get_pr_diff.json +5 -0
  25. package/.mcp/schemas/github_get_pr_reviews.json +5 -0
  26. package/.mcp/schemas/github_issue_comment.json +5 -0
  27. package/.mcp/schemas/github_issue_update.json +5 -0
  28. package/.mcp/schemas/github_issue_view.json +5 -0
  29. package/.mcp/schemas/github_open_replacement_pr.json +5 -0
  30. package/.mcp/schemas/github_search_open_prs.json +5 -0
  31. package/.mcp/schemas/jules.cancel_session.json +5 -0
  32. package/.mcp/schemas/jules.create_session.json +5 -0
  33. package/.mcp/schemas/jules.get_messages.json +5 -0
  34. package/.mcp/schemas/jules.get_pr.json +5 -0
  35. package/.mcp/schemas/jules.get_session.json +5 -0
  36. package/.mcp/schemas/jules.list_sessions.json +5 -0
  37. package/.mcp/schemas/jules.send_message.json +5 -0
  38. package/.mcp/schemas/jules.trigger_feedback.json +5 -0
  39. package/.mcp/schemas/jules_cancel_session.json +5 -0
  40. package/.mcp/schemas/jules_create_session.json +5 -0
  41. package/.mcp/schemas/jules_get_messages.json +5 -0
  42. package/.mcp/schemas/jules_get_pr.json +5 -0
  43. package/.mcp/schemas/jules_get_session.json +5 -0
  44. package/.mcp/schemas/jules_list_sessions.json +5 -0
  45. package/.mcp/schemas/jules_send_message.json +5 -0
  46. package/.mcp/schemas/jules_trigger_feedback.json +5 -0
  47. package/.mcp/schemas/repo.commit_patch.json +5 -0
  48. package/.mcp/schemas/repo.create_branch.json +5 -0
  49. package/.mcp/schemas/repo.create_repair_branch.json +5 -0
  50. package/.mcp/schemas/repo.get_changed_files.json +5 -0
  51. package/.mcp/schemas/repo.get_command_schema.json +6 -1
  52. package/.mcp/schemas/repo.get_package_scripts.json +5 -0
  53. package/.mcp/schemas/repo.get_workflow_runs.json +5 -0
  54. package/.mcp/schemas/repo.logs.json +5 -0
  55. package/.mcp/schemas/repo.read_agent_context.json +5 -0
  56. package/.mcp/schemas/repo.read_ci_logs.json +5 -0
  57. package/.mcp/schemas/repo.run_tests.json +5 -0
  58. package/.mcp/schemas/repo_commit_patch.json +5 -0
  59. package/.mcp/schemas/repo_create_branch.json +5 -0
  60. package/.mcp/schemas/repo_create_repair_branch.json +5 -0
  61. package/.mcp/schemas/repo_get_changed_files.json +5 -0
  62. package/.mcp/schemas/repo_get_command_schema.json +6 -1
  63. package/.mcp/schemas/repo_get_package_scripts.json +5 -0
  64. package/.mcp/schemas/repo_get_workflow_runs.json +5 -0
  65. package/.mcp/schemas/repo_logs.json +5 -0
  66. package/.mcp/schemas/repo_read_agent_context.json +5 -0
  67. package/.mcp/schemas/repo_read_ci_logs.json +5 -0
  68. package/.mcp/schemas/repo_run_tests.json +5 -0
  69. package/CHANGELOG.md +7 -0
  70. package/actions/ai-review/action.yml +2 -2
  71. package/actions/audit/action.yml +1 -1
  72. package/actions/ci-validate/action.yml +2 -2
  73. package/actions/impact-analysis/action.yml +3 -3
  74. package/actions/run-project-gate/action.yml +5 -5
  75. package/actions/setup/action.yml +1 -1
  76. package/actions/test-build/action.yml +1 -1
  77. package/dist/config.js +1 -1
  78. package/dist/config.test.js +3 -3
  79. package/dist/lib/result.js +4 -0
  80. package/dist/lib/result.test.js +4 -2
  81. package/dist/lib/shell.js +6 -6
  82. package/dist/mcp/definitions.js +73 -2
  83. package/dist/mcp/http.js +91 -0
  84. package/dist/mcp/http.test.js +58 -0
  85. package/dist/mcp/server.js +53 -13
  86. package/dist/mcp/server.test.js +20 -0
  87. package/dist/tools/contract.js +109 -1
  88. package/dist/tools/github.checkout_branch.js +1 -1
  89. package/dist/tools/github.comment_triage_summary.js +1 -1
  90. package/dist/tools/github.create_issue.js +1 -1
  91. package/dist/tools/github.create_issue.test.js +6 -6
  92. package/dist/tools/github.create_pull_request.js +1 -1
  93. package/dist/tools/github.create_pull_request.test.js +4 -4
  94. package/dist/tools/github.get_pr.js +1 -1
  95. package/dist/tools/github.get_pr_commits.js +1 -1
  96. package/dist/tools/github.get_pr_diff.js +1 -1
  97. package/dist/tools/github.get_pr_reviews.js +1 -1
  98. package/dist/tools/github.get_pr_reviews.test.js +9 -9
  99. package/dist/tools/github.issue_comment.js +1 -1
  100. package/dist/tools/github.issue_update.js +1 -1
  101. package/dist/tools/github.issue_view.js +1 -1
  102. package/dist/tools/github.search_open_prs.js +1 -1
  103. package/dist/tools/jules/cancel-session.js +1 -1
  104. package/dist/tools/jules/create-session.test.js +11 -11
  105. package/dist/tools/jules/get-messages.js +1 -1
  106. package/dist/tools/jules/get-messages.test.js +3 -3
  107. package/dist/tools/jules/get-pr.test.js +3 -3
  108. package/dist/tools/jules/list-sessions.js +1 -1
  109. package/dist/tools/jules/send-message.js +1 -1
  110. package/dist/tools/jules/send-message.test.js +5 -5
  111. package/dist/tools/jules/shared.js +4 -4
  112. package/dist/tools/jules/trigger-feedback.js +1 -1
  113. package/dist/tools/jules/trigger-feedback.test.js +5 -5
  114. package/dist/tools/repo.get_command_schema.js +2 -2
  115. package/dist/tools/repo.get_workflow_runs.js +1 -1
  116. package/package.json +1 -1
  117. package/scripts/sync-mcp-schemas.ts +2 -0
  118. package/src/config.test.ts +3 -3
  119. package/src/config.ts +1 -1
  120. package/src/lib/result.test.ts +4 -2
  121. package/src/lib/result.ts +6 -0
  122. package/src/lib/shell.ts +6 -6
  123. package/src/mcp/definitions.ts +76 -3
  124. package/src/mcp/http.test.ts +72 -0
  125. package/src/mcp/http.ts +118 -0
  126. package/src/mcp/server.test.ts +24 -0
  127. package/src/mcp/server.ts +63 -12
  128. package/src/tools/contract.ts +109 -1
  129. package/src/tools/github.checkout_branch.ts +1 -1
  130. package/src/tools/github.comment_triage_summary.ts +1 -1
  131. package/src/tools/github.create_issue.test.ts +6 -6
  132. package/src/tools/github.create_issue.ts +1 -1
  133. package/src/tools/github.create_pull_request.test.ts +4 -4
  134. package/src/tools/github.create_pull_request.ts +1 -1
  135. package/src/tools/github.get_pr.ts +1 -1
  136. package/src/tools/github.get_pr_commits.ts +1 -1
  137. package/src/tools/github.get_pr_diff.ts +1 -1
  138. package/src/tools/github.get_pr_reviews.test.ts +9 -9
  139. package/src/tools/github.get_pr_reviews.ts +1 -1
  140. package/src/tools/github.issue_comment.ts +1 -1
  141. package/src/tools/github.issue_update.ts +1 -1
  142. package/src/tools/github.issue_view.ts +1 -1
  143. package/src/tools/github.search_open_prs.ts +1 -1
  144. package/src/tools/jules/cancel-session.ts +1 -1
  145. package/src/tools/jules/create-session.test.ts +11 -11
  146. package/src/tools/jules/get-messages.test.ts +3 -3
  147. package/src/tools/jules/get-messages.ts +1 -1
  148. package/src/tools/jules/get-pr.test.ts +3 -3
  149. package/src/tools/jules/list-sessions.ts +1 -1
  150. package/src/tools/jules/send-message.test.ts +5 -5
  151. package/src/tools/jules/send-message.ts +1 -1
  152. package/src/tools/jules/shared.ts +4 -4
  153. package/src/tools/jules/trigger-feedback.test.ts +5 -5
  154. package/src/tools/jules/trigger-feedback.ts +1 -1
  155. package/src/tools/repo.get_command_schema.ts +2 -2
  156. package/src/tools/repo.get_workflow_runs.ts +1 -1
@@ -26,6 +26,24 @@ export const MCP_PROMPTS = [
26
26
  },
27
27
  ];
28
28
  export const MCP_RESOURCES = [
29
+ {
30
+ uri: "mcp://workspace/agent-contract",
31
+ name: "Agent Contract",
32
+ mimeType: "text/markdown",
33
+ description: "The agent contract specification file (.agents/AGENT_CONTRACT.md).",
34
+ },
35
+ {
36
+ uri: "repo://agent-contract",
37
+ name: "Agent Contract (Legacy URI)",
38
+ mimeType: "text/markdown",
39
+ description: "The agent contract specification file (.agents/AGENT_CONTRACT.md).",
40
+ },
41
+ {
42
+ uri: "mcp://git/changed-files",
43
+ name: "Git Changed Files",
44
+ mimeType: "application/json",
45
+ description: "List of changed files in the repository.",
46
+ },
29
47
  {
30
48
  uri: "repo://package-json",
31
49
  name: "package.json",
@@ -38,183 +56,236 @@ export const MCP_RESOURCES = [
38
56
  mimeType: "application/json",
39
57
  description: "The design tokens used in the repository.",
40
58
  },
59
+ ];
60
+ export const MCP_RESOURCE_TEMPLATES = [
61
+ {
62
+ uriTemplate: "mcp://git/diff/{prNumber}",
63
+ name: "Git PR Diff",
64
+ mimeType: "text/plain",
65
+ description: "Diff and changed files for a pull request.",
66
+ },
67
+ {
68
+ uriTemplate: "mcp://ci/logs/{prNumber}",
69
+ name: "CI Logs",
70
+ mimeType: "application/json",
71
+ description: "CI logs for a pull request.",
72
+ },
41
73
  {
42
- uri: "repo://repair-report/{branch}",
74
+ uriTemplate: "repo://repair-report/{branch}",
43
75
  name: "Repair Report",
44
76
  mimeType: "application/json",
45
77
  description: "The validation report for a specific repair branch.",
46
78
  },
47
79
  ];
80
+ const DEFAULT_OUTPUT_SCHEMA = {
81
+ type: "object",
82
+ properties: {},
83
+ additionalProperties: true,
84
+ };
48
85
  const BASE_MCP_TOOLS = [
49
86
  {
50
87
  name: "boomtick.health",
51
88
  description: "Check the health and configuration of the MCP server.",
52
89
  inputSchema: HealthCheckInputJsonSchema,
90
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
53
91
  },
54
92
  {
55
93
  name: "github.search_open_prs",
56
94
  description: "Search for open pull requests in the repository.",
57
95
  inputSchema: SearchPRsInputJsonSchema,
96
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
58
97
  },
59
98
  {
60
99
  name: "repo.read_agent_context",
61
100
  description: "Read the full repository index including file tree, CLI schema, and MCP tools.",
62
101
  inputSchema: ReadAgentContextSchema,
102
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
63
103
  },
64
104
  {
65
105
  name: "github.get_pr_diff",
66
106
  description: "Get the diff and changed files for a pull request.",
67
107
  inputSchema: GetPrDiffInputJsonSchema,
108
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
68
109
  },
69
110
  {
70
111
  name: "github.checkout_branch",
71
112
  description: "Checkout a specific branch in the repository or worktree.",
72
113
  inputSchema: CheckoutBranchInputJsonSchema,
114
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
73
115
  },
74
116
  {
75
117
  name: "github.get_merge_conflict_files",
76
118
  description: "Detect files that conflict when a PR is merged with the base branch.",
77
119
  inputSchema: GetMergeConflictFilesInputJsonSchema,
120
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
78
121
  },
79
122
  {
80
123
  name: "repo.get_changed_files",
81
124
  description: "Get the list of changed files between two refs.",
82
125
  inputSchema: GetChangedFilesInputJsonSchema,
126
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
83
127
  },
84
128
  {
85
129
  name: "repo.get_command_schema",
86
- description: "Retrieve a lightweight, targeted schema for a specific td-cli subcommand. Use this instead of reading the entire cli-schema.json to save tokens.",
130
+ description: "Retrieve a lightweight, targeted schema for a specific td subcommand. Use this instead of reading the entire cli-schema.json to save tokens.",
87
131
  inputSchema: GetCommandSchemaInputJsonSchema,
132
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
88
133
  },
89
134
  {
90
135
  name: "repo.get_package_scripts",
91
136
  description: "Get the scripts defined in package.json.",
92
137
  inputSchema: GetPackageScriptsInputJsonSchema,
138
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
93
139
  },
94
140
  {
95
141
  name: "repo.read_ci_logs",
96
142
  description: "Read CI logs for a given pull request.",
97
143
  inputSchema: ReadCiLogsInputJsonSchema,
144
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
98
145
  },
99
146
  {
100
147
  name: "repo.logs",
101
148
  description: "Stream or grep combined CI logs for all jobs in a pull request.",
102
149
  inputSchema: RepoLogsInputJsonSchema,
150
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
103
151
  },
104
152
  {
105
153
  name: "repo.create_branch",
106
154
  description: "Creates a new clean git branch from a target base branch.",
107
155
  inputSchema: CreateBranchInputJsonSchema,
156
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
108
157
  },
109
158
  {
110
159
  name: "repo.create_repair_branch",
111
160
  description: "Creates a fresh worktree and a new repair branch for an existing PR. This isolates the repair work from the main codebase.",
112
161
  inputSchema: CreateRepairBranchInputJsonSchema,
162
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
113
163
  },
114
164
  {
115
165
  name: "repo.run_tests",
116
166
  description: "Run repository tests and checks.",
117
167
  inputSchema: RunTestsInputJsonSchema,
168
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
118
169
  },
119
170
  {
120
171
  name: "repo.commit_patch",
121
172
  description: "Commit verified repair changes in a worktree.",
122
173
  inputSchema: CommitPatchInputJsonSchema,
174
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
123
175
  },
124
176
  {
125
177
  name: "github.open_replacement_pr",
126
178
  description: "Open a new PR that replaces or repairs the original PR.",
127
179
  inputSchema: OpenReplacementPrInputJsonSchema,
180
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
128
181
  },
129
182
  {
130
183
  name: "github.create_pull_request",
131
184
  description: "Creates a pull request on GitHub using the MCP server's integrated credentials. Bypasses terminal CLI constraints.",
132
185
  inputSchema: CreatePullRequestInputJsonSchema,
186
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
133
187
  },
134
188
  {
135
189
  name: "github.comment_triage_summary",
136
190
  description: "Comment on the original PR with a diagnosis and replacement link.",
137
191
  inputSchema: CommentTriageSummaryInputJsonSchema,
192
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
138
193
  },
139
194
  {
140
195
  name: "github.get_pr",
141
196
  description: "Get details of a GitHub PR including title, state, and URLs.",
142
197
  inputSchema: GetPrInputJsonSchema,
198
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
143
199
  },
144
200
  {
145
201
  name: "github.get_pr_reviews",
146
202
  description: "Get top-level reviews, verdicts, states, and bodies for a GitHub PR.",
147
203
  inputSchema: GetPrReviewsInputJsonSchema,
204
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
148
205
  },
149
206
  {
150
207
  name: "github.get_pr_commits",
151
208
  description: "List commits and commit metadata (SHAs, authors, messages) for a GitHub PR.",
152
209
  inputSchema: GetPrCommitsInputJsonSchema,
210
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
153
211
  },
154
212
  {
155
213
  name: "repo.get_workflow_runs",
156
214
  description: "List recent GitHub Actions workflow runs for the repository.",
157
215
  inputSchema: GetWorkflowRunsInputJsonSchema,
216
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
158
217
  },
159
218
  {
160
219
  name: "github.issue_view",
161
220
  description: "View details of a GitHub issue including title, body, and state.",
162
221
  inputSchema: IssueViewInputJsonSchema,
222
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
163
223
  },
164
224
  {
165
225
  name: "github.issue_update",
166
226
  description: "Update a GitHub issue's body, labels, and/or state.",
167
227
  inputSchema: IssueUpdateInputJsonSchema,
228
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
168
229
  },
169
230
  {
170
231
  name: "github.issue_comment",
171
232
  description: "Add a new comment to a GitHub issue.",
172
233
  inputSchema: IssueCommentInputJsonSchema,
234
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
173
235
  },
174
236
  {
175
237
  name: "github.create_issue",
176
238
  description: "Create a new GitHub issue.",
177
239
  inputSchema: CreateIssueInputJsonSchema,
240
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
178
241
  },
179
242
  {
180
243
  name: "jules.create_session",
181
244
  description: "Create a Jules session that performs work externally and may generate a GitHub pull request.",
182
245
  inputSchema: CreateJulesSessionInputJsonSchema,
246
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
183
247
  },
184
248
  {
185
249
  name: "jules.get_session",
186
250
  description: "Get the status and details of a Jules session.",
187
251
  inputSchema: JulesSessionIdInputJsonSchema,
252
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
188
253
  },
189
254
  {
190
255
  name: "jules.send_message",
191
256
  description: "Send a message to an active Jules session.",
192
257
  inputSchema: JulesSendMessageInputJsonSchema,
258
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
193
259
  },
194
260
  {
195
261
  name: "jules.get_messages",
196
262
  description: "Get the message history of a Jules session.",
197
263
  inputSchema: JulesSessionIdInputJsonSchema,
264
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
198
265
  },
199
266
  {
200
267
  name: "jules.list_sessions",
201
268
  description: "List all Jules sessions.",
202
269
  inputSchema: JulesListSessionsInputJsonSchema,
270
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
203
271
  },
204
272
  {
205
273
  name: "jules.cancel_session",
206
274
  description: "Cancel an ongoing Jules session.",
207
275
  inputSchema: JulesSessionIdInputJsonSchema,
276
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
208
277
  },
209
278
  {
210
279
  name: "jules.get_pr",
211
280
  description: "Get the generated pull request url associated with an active Jules agent session.",
212
281
  inputSchema: JulesSessionIdInputJsonSchema,
282
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
213
283
  },
214
284
  {
215
285
  name: "jules.trigger_feedback",
216
286
  description: "Automatically collect CI status/logs for the PR associated with a Jules session and send them back as feedback.",
217
287
  inputSchema: JulesSessionIdInputJsonSchema,
288
+ outputSchema: DEFAULT_OUTPUT_SCHEMA,
218
289
  },
219
290
  ];
220
291
  export const MCP_TOOLS = BASE_MCP_TOOLS.flatMap((tool) => {
@@ -0,0 +1,91 @@
1
+ import http from "node:http";
2
+ import crypto from "node:crypto";
3
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
+ export function createMcpHttpServer(options) {
5
+ const { port = 3000, bearerToken = process.env.BOOMTICK_MCP_BEARER_TOKEN || process.env.MCP_BEARER_TOKEN || process.env.AUTH_TOKEN, mcpServer, } = options;
6
+ const transports = new Map();
7
+ const server = http.createServer(async (req, res) => {
8
+ // CORS Headers
9
+ res.setHeader("Access-Control-Allow-Origin", "*");
10
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
11
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
12
+ if (req.method === "OPTIONS") {
13
+ res.writeHead(204);
14
+ res.end();
15
+ return;
16
+ }
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": [
24
+ process.env.MCP_OAUTH_AUTH_SERVER || "https://auth.boomtick.dev",
25
+ ],
26
+ scopes_supported: ["mcp:read", "mcp:write"],
27
+ }));
28
+ return;
29
+ }
30
+ // Bearer Token Validation for SSE & Messages endpoints
31
+ if (url.pathname === "/sse" || url.pathname === "/messages") {
32
+ if (!bearerToken) {
33
+ res.writeHead(500, { "Content-Type": "application/json" });
34
+ res.end(JSON.stringify({ error: "Server misconfiguration: bearerToken is required for HTTP transport" }));
35
+ return;
36
+ }
37
+ const authHeader = req.headers.authorization;
38
+ const expectedAuth = `Bearer ${bearerToken}`;
39
+ const authBuffer = Buffer.from(authHeader || "");
40
+ const expectedBuffer = Buffer.from(expectedAuth);
41
+ const isMatch = Boolean(authHeader) &&
42
+ authBuffer.length === expectedBuffer.length &&
43
+ crypto.timingSafeEqual(authBuffer, expectedBuffer);
44
+ if (!isMatch) {
45
+ res.writeHead(401, {
46
+ "Content-Type": "application/json",
47
+ "WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token"',
48
+ });
49
+ res.end(JSON.stringify({ error: "unauthorized", message: "Invalid or missing Bearer token" }));
50
+ return;
51
+ }
52
+ }
53
+ // Endpoint 2: SSE stream setup
54
+ if (url.pathname === "/sse" && req.method === "GET") {
55
+ const transport = new SSEServerTransport("/messages", res);
56
+ transports.set(transport.sessionId, transport);
57
+ transport.onclose = () => {
58
+ transports.delete(transport.sessionId);
59
+ };
60
+ await mcpServer.connect(transport);
61
+ return;
62
+ }
63
+ // Endpoint 3: POST messages
64
+ if (url.pathname === "/messages" && req.method === "POST") {
65
+ const sessionId = url.searchParams.get("sessionId");
66
+ const transport = sessionId ? transports.get(sessionId) : null;
67
+ if (transport) {
68
+ await transport.handlePostMessage(req, res);
69
+ }
70
+ else {
71
+ res.writeHead(400, { "Content-Type": "application/json" });
72
+ res.end(JSON.stringify({ error: "Session not found or expired" }));
73
+ }
74
+ return;
75
+ }
76
+ res.writeHead(404, { "Content-Type": "application/json" });
77
+ res.end(JSON.stringify({ error: "Not found" }));
78
+ });
79
+ return {
80
+ server,
81
+ listen: (customPort) => new Promise((resolve) => {
82
+ const p = customPort ?? port;
83
+ server.listen(p, () => {
84
+ const addr = server.address();
85
+ const actualPort = typeof addr === "object" && addr ? addr.port : p;
86
+ console.error(`Boomtick MCP Server running on HTTP port ${actualPort}`);
87
+ resolve(actualPort);
88
+ });
89
+ }),
90
+ };
91
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { createMcpHttpServer } from "./http.js";
4
+ describe("createMcpHttpServer", () => {
5
+ let mcpServer;
6
+ let httpServerObj;
7
+ let port;
8
+ beforeEach(async () => {
9
+ mcpServer = new Server({ name: "test-server", version: "1.0.0" }, { capabilities: {} });
10
+ });
11
+ afterEach(() => {
12
+ if (httpServerObj?.server) {
13
+ httpServerObj.server.close();
14
+ }
15
+ });
16
+ it("should respond with OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
17
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
18
+ port = await httpServerObj.listen(0);
19
+ const res = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`);
20
+ expect(res.status).toBe(200);
21
+ const body = await res.json();
22
+ expect(body.resource).toBe("mcp://boomtick-mcp");
23
+ expect(body.authorization_servers).toBeDefined();
24
+ expect(body.scopes_supported).toEqual(["mcp:read", "mcp:write"]);
25
+ });
26
+ it("should reject requests when Bearer token is missing or invalid", async () => {
27
+ httpServerObj = createMcpHttpServer({ port: 0, bearerToken: "secret123", mcpServer });
28
+ port = await httpServerObj.listen(0);
29
+ // Request without token
30
+ const res1 = await fetch(`http://localhost:${port}/sse`);
31
+ expect(res1.status).toBe(401);
32
+ expect(res1.headers.get("www-authenticate")).toContain('Bearer realm="boomtick-mcp"');
33
+ // Request with wrong token
34
+ const res2 = await fetch(`http://localhost:${port}/sse`, {
35
+ headers: { Authorization: "Bearer wrong" },
36
+ });
37
+ expect(res2.status).toBe(401);
38
+ // Request with valid token
39
+ const res3 = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`, {
40
+ headers: { Authorization: "Bearer secret123" },
41
+ });
42
+ expect(res3.status).toBe(200);
43
+ });
44
+ it("should handle 404 for unknown endpoints", async () => {
45
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
46
+ port = await httpServerObj.listen(0);
47
+ const res = await fetch(`http://localhost:${port}/unknown-path`);
48
+ expect(res.status).toBe(404);
49
+ });
50
+ it("should return 500 if bearer token is not configured on server for protected endpoints", async () => {
51
+ httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
52
+ port = await httpServerObj.listen(0);
53
+ const res = await fetch(`http://localhost:${port}/sse`);
54
+ expect(res.status).toBe(500);
55
+ const body = await res.json();
56
+ expect(body.error).toContain("Server misconfiguration");
57
+ });
58
+ });
@@ -1,9 +1,9 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { config } from "../config.js";
5
5
  import { createSuccessResult, createErrorResult } from "../lib/result.js";
6
- import { MCP_TOOLS, MCP_PROMPTS, MCP_RESOURCES } from "./definitions.js";
6
+ import { MCP_TOOLS, MCP_PROMPTS, MCP_RESOURCES, MCP_RESOURCE_TEMPLATES } from "./definitions.js";
7
7
  import { normalizeArguments } from "../lib/param_utils.js";
8
8
  import { healthHandler, HealthCheckInputSchema } from "./tools.js";
9
9
  import { searchOpenPrsHandler, SearchOpenPrsInputSchema } from "../tools/github.search_open_prs.js";
@@ -40,6 +40,7 @@ import fs from "fs/promises";
40
40
  import { existsSync, readFileSync } from "fs";
41
41
  import path from "path";
42
42
  import { spawnSync } from "child_process";
43
+ import { createMcpHttpServer } from "./http.js";
43
44
  export class BoomtickMCPServer {
44
45
  server;
45
46
  constructor() {
@@ -93,29 +94,50 @@ export class BoomtickMCPServer {
93
94
  resources: MCP_RESOURCES,
94
95
  };
95
96
  });
97
+ this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
98
+ return {
99
+ resourceTemplates: MCP_RESOURCE_TEMPLATES,
100
+ };
101
+ });
96
102
  this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
97
103
  const uri = request.params.uri;
98
- if (uri === "repo://package-json") {
104
+ if (uri === "mcp://workspace/agent-contract" ||
105
+ uri === "mcp://workspace/agent_contract" ||
106
+ uri === "repo://agent-contract") {
107
+ const contractPath = path.join(config.repoPath, ".agents", "AGENT_CONTRACT.md");
108
+ const content = await fs.readFile(contractPath, "utf-8");
109
+ return {
110
+ contents: [{ uri, mimeType: "text/markdown", text: content }],
111
+ };
112
+ }
113
+ if (uri === "mcp://git/changed-files" ||
114
+ uri === "repo://changed-files") {
115
+ const changedFiles = await getChangedFilesHandler(GetChangedFilesInputSchema.parse({}));
116
+ return {
117
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(changedFiles, null, 2) }],
118
+ };
119
+ }
120
+ if (uri === "repo://package-json" || uri === "mcp://workspace/package-json") {
99
121
  const content = await fs.readFile(path.join(config.repoPath, "package.json"), "utf-8");
100
122
  return {
101
123
  contents: [{ uri, mimeType: "application/json", text: content }],
102
124
  };
103
125
  }
104
- if (uri === "repo://design-tokens") {
126
+ if (uri === "repo://design-tokens" || uri === "mcp://workspace/design-tokens") {
105
127
  const tokensPath = path.join(config.repoPath, "src/styles/design-tokens.ts");
106
128
  const content = await fs.readFile(tokensPath, "utf-8");
107
129
  return {
108
130
  contents: [{ uri, mimeType: "text/typescript", text: content }],
109
131
  };
110
132
  }
111
- if (uri.startsWith("repo://diff/")) {
133
+ if (uri.startsWith("mcp://git/diff/") || uri.startsWith("repo://diff/")) {
112
134
  const prNumber = parseInt(uri.split("/").pop() || "");
113
135
  const diff = await getPrDiffHandler({ prNumber });
114
136
  return {
115
137
  contents: [{ uri, mimeType: "text/plain", text: diff.diffText }],
116
138
  };
117
139
  }
118
- if (uri.startsWith("repo://ci/")) {
140
+ if (uri.startsWith("mcp://ci/logs/") || uri.startsWith("repo://ci/")) {
119
141
  const prNumber = parseInt(uri.split("/").pop() || "");
120
142
  const logs = await readCiLogsHandler({ prNumber });
121
143
  return {
@@ -236,7 +258,7 @@ export class BoomtickMCPServer {
236
258
  }
237
259
  });
238
260
  }
239
- async run() {
261
+ prepareEnvironmentAndPreflight() {
240
262
  const projectRoot = config.repoPath || process.cwd();
241
263
  const homeDir = process.env.HOME || "";
242
264
  const extraPaths = [];
@@ -276,9 +298,9 @@ export class BoomtickMCPServer {
276
298
  .join(path.delimiter);
277
299
  // 3. SAFE REPOSITORY RESOLUTION
278
300
  const repoPath = config.repoPath || projectRoot;
279
- // 4. SECURE, UNIX-ONLY PRE-FLIGHT CHECK FOR TD-CLI
301
+ // 4. SECURE, UNIX-ONLY PRE-FLIGHT CHECK FOR TD
280
302
  try {
281
- const result = spawnSync("td-cli", ["doctor"], {
303
+ const result = spawnSync("td", ["doctor"], {
282
304
  encoding: "utf-8",
283
305
  cwd: repoPath,
284
306
  shell: false // Explicitly disabled to clear Semgrep rules
@@ -290,16 +312,34 @@ export class BoomtickMCPServer {
290
312
  console.error("status:", result.status);
291
313
  console.error("stdout:", result.stdout);
292
314
  console.error("stderr:", result.stderr);
293
- throw new Error(`td-cli doctor returned exit code: ${result.status}`);
315
+ throw new Error(`td doctor returned exit code: ${result.status}`);
294
316
  }
295
- console.error("✅ td-cli verified on PATH securely");
317
+ console.error("✅ td verified on PATH securely");
296
318
  }
297
319
  catch (error) {
298
- console.error("❌ Fatal: td-cli is not resolvable on PATH. MCP tools will fail.");
320
+ console.error("❌ Fatal: td is not resolvable on PATH. MCP tools will fail.");
299
321
  console.error("Please ensure the project is installed: pip install -e .");
300
- console.error("❌ Fatal: td-cli pre-flight check failed:", error instanceof Error ? error.message : String(error));
322
+ console.error("❌ Fatal: td pre-flight check failed:", error instanceof Error ? error.message : String(error));
301
323
  process.exit(1);
302
324
  }
325
+ }
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
+ async run() {
332
+ this.prepareEnvironmentAndPreflight();
333
+ 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"));
337
+ if (isHttp) {
338
+ const port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
339
+ const mcpHttp = createMcpHttpServer({ port, mcpServer: this.server });
340
+ await mcpHttp.listen(port);
341
+ return;
342
+ }
303
343
  const transport = new StdioServerTransport();
304
344
  await this.server.connect(transport);
305
345
  console.error("Boomtick MCP Server running on stdio");
@@ -83,8 +83,27 @@ describe("BoomtickMCPServer", () => {
83
83
  expect(result.resources).toBeDefined();
84
84
  expect(Array.isArray(result.resources)).toBe(true);
85
85
  });
86
+ it("should return resource templates list", async () => {
87
+ const handler = requestHandlers.get("resources/templates/list");
88
+ expect(handler).toBeDefined();
89
+ const result = await handler({ method: "resources/templates/list" });
90
+ expect(result.resourceTemplates).toBeDefined();
91
+ expect(Array.isArray(result.resourceTemplates)).toBe(true);
92
+ });
86
93
  });
87
94
  describe("ReadResource", () => {
95
+ it("should handle mcp://workspace/agent-contract", async () => {
96
+ const handler = requestHandlers.get("resources/read");
97
+ vi.mocked(fs.readFile).mockResolvedValue("# Agent Contract\nRule 1");
98
+ const result = await handler({
99
+ method: "resources/read",
100
+ params: { uri: "mcp://workspace/agent-contract" },
101
+ });
102
+ expect(fs.readFile).toHaveBeenCalledWith("/mock/repo/.agents/AGENT_CONTRACT.md", "utf-8");
103
+ expect(result).toEqual({
104
+ contents: [{ uri: "mcp://workspace/agent-contract", mimeType: "text/markdown", text: "# Agent Contract\nRule 1" }],
105
+ });
106
+ });
88
107
  it("should handle repo://package-json", async () => {
89
108
  const handler = requestHandlers.get("resources/read");
90
109
  vi.mocked(fs.readFile).mockResolvedValue('{"name": "test"}');
@@ -155,6 +174,7 @@ describe("BoomtickMCPServer", () => {
155
174
  });
156
175
  expect(result).toEqual({
157
176
  content: [{ type: "text", text: JSON.stringify({ status: "ok" }, null, 2) }],
177
+ structuredContent: { status: "ok" },
158
178
  });
159
179
  });
160
180
  it("should return error result for unknown tool", async () => {