@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.
- package/.mcp/schemas/boomtick.health.json +5 -0
- package/.mcp/schemas/boomtick_health.json +5 -0
- package/.mcp/schemas/github.checkout_branch.json +5 -0
- package/.mcp/schemas/github.comment_triage_summary.json +5 -0
- package/.mcp/schemas/github.create_issue.json +5 -0
- package/.mcp/schemas/github.create_pull_request.json +5 -0
- package/.mcp/schemas/github.get_merge_conflict_files.json +5 -0
- package/.mcp/schemas/github.get_pr.json +5 -0
- package/.mcp/schemas/github.get_pr_commits.json +5 -0
- package/.mcp/schemas/github.get_pr_diff.json +5 -0
- package/.mcp/schemas/github.get_pr_reviews.json +5 -0
- package/.mcp/schemas/github.issue_comment.json +5 -0
- package/.mcp/schemas/github.issue_update.json +5 -0
- package/.mcp/schemas/github.issue_view.json +5 -0
- package/.mcp/schemas/github.open_replacement_pr.json +5 -0
- package/.mcp/schemas/github.search_open_prs.json +5 -0
- package/.mcp/schemas/github_checkout_branch.json +5 -0
- package/.mcp/schemas/github_comment_triage_summary.json +5 -0
- package/.mcp/schemas/github_create_issue.json +5 -0
- package/.mcp/schemas/github_create_pull_request.json +5 -0
- package/.mcp/schemas/github_get_merge_conflict_files.json +5 -0
- package/.mcp/schemas/github_get_pr.json +5 -0
- package/.mcp/schemas/github_get_pr_commits.json +5 -0
- package/.mcp/schemas/github_get_pr_diff.json +5 -0
- package/.mcp/schemas/github_get_pr_reviews.json +5 -0
- package/.mcp/schemas/github_issue_comment.json +5 -0
- package/.mcp/schemas/github_issue_update.json +5 -0
- package/.mcp/schemas/github_issue_view.json +5 -0
- package/.mcp/schemas/github_open_replacement_pr.json +5 -0
- package/.mcp/schemas/github_search_open_prs.json +5 -0
- package/.mcp/schemas/jules.cancel_session.json +5 -0
- package/.mcp/schemas/jules.create_session.json +5 -0
- package/.mcp/schemas/jules.get_messages.json +5 -0
- package/.mcp/schemas/jules.get_pr.json +5 -0
- package/.mcp/schemas/jules.get_session.json +5 -0
- package/.mcp/schemas/jules.list_sessions.json +5 -0
- package/.mcp/schemas/jules.send_message.json +5 -0
- package/.mcp/schemas/jules.trigger_feedback.json +5 -0
- package/.mcp/schemas/jules_cancel_session.json +5 -0
- package/.mcp/schemas/jules_create_session.json +5 -0
- package/.mcp/schemas/jules_get_messages.json +5 -0
- package/.mcp/schemas/jules_get_pr.json +5 -0
- package/.mcp/schemas/jules_get_session.json +5 -0
- package/.mcp/schemas/jules_list_sessions.json +5 -0
- package/.mcp/schemas/jules_send_message.json +5 -0
- package/.mcp/schemas/jules_trigger_feedback.json +5 -0
- package/.mcp/schemas/repo.commit_patch.json +5 -0
- package/.mcp/schemas/repo.create_branch.json +5 -0
- package/.mcp/schemas/repo.create_repair_branch.json +5 -0
- package/.mcp/schemas/repo.get_changed_files.json +5 -0
- package/.mcp/schemas/repo.get_command_schema.json +6 -1
- package/.mcp/schemas/repo.get_package_scripts.json +5 -0
- package/.mcp/schemas/repo.get_workflow_runs.json +5 -0
- package/.mcp/schemas/repo.logs.json +5 -0
- package/.mcp/schemas/repo.read_agent_context.json +5 -0
- package/.mcp/schemas/repo.read_ci_logs.json +5 -0
- package/.mcp/schemas/repo.run_tests.json +5 -0
- package/.mcp/schemas/repo_commit_patch.json +5 -0
- package/.mcp/schemas/repo_create_branch.json +5 -0
- package/.mcp/schemas/repo_create_repair_branch.json +5 -0
- package/.mcp/schemas/repo_get_changed_files.json +5 -0
- package/.mcp/schemas/repo_get_command_schema.json +6 -1
- package/.mcp/schemas/repo_get_package_scripts.json +5 -0
- package/.mcp/schemas/repo_get_workflow_runs.json +5 -0
- package/.mcp/schemas/repo_logs.json +5 -0
- package/.mcp/schemas/repo_read_agent_context.json +5 -0
- package/.mcp/schemas/repo_read_ci_logs.json +5 -0
- package/.mcp/schemas/repo_run_tests.json +5 -0
- package/CHANGELOG.md +7 -0
- package/actions/ai-review/action.yml +2 -2
- package/actions/audit/action.yml +1 -1
- package/actions/ci-validate/action.yml +2 -2
- package/actions/impact-analysis/action.yml +3 -3
- package/actions/run-project-gate/action.yml +5 -5
- package/actions/setup/action.yml +1 -1
- package/actions/test-build/action.yml +1 -1
- package/dist/config.js +1 -1
- package/dist/config.test.js +3 -3
- package/dist/lib/result.js +4 -0
- package/dist/lib/result.test.js +4 -2
- package/dist/lib/shell.js +6 -6
- package/dist/mcp/definitions.js +73 -2
- package/dist/mcp/http.js +91 -0
- package/dist/mcp/http.test.js +58 -0
- package/dist/mcp/server.js +53 -13
- package/dist/mcp/server.test.js +20 -0
- package/dist/tools/contract.js +109 -1
- package/dist/tools/github.checkout_branch.js +1 -1
- package/dist/tools/github.comment_triage_summary.js +1 -1
- package/dist/tools/github.create_issue.js +1 -1
- package/dist/tools/github.create_issue.test.js +6 -6
- package/dist/tools/github.create_pull_request.js +1 -1
- package/dist/tools/github.create_pull_request.test.js +4 -4
- package/dist/tools/github.get_pr.js +1 -1
- package/dist/tools/github.get_pr_commits.js +1 -1
- package/dist/tools/github.get_pr_diff.js +1 -1
- package/dist/tools/github.get_pr_reviews.js +1 -1
- package/dist/tools/github.get_pr_reviews.test.js +9 -9
- package/dist/tools/github.issue_comment.js +1 -1
- package/dist/tools/github.issue_update.js +1 -1
- package/dist/tools/github.issue_view.js +1 -1
- package/dist/tools/github.search_open_prs.js +1 -1
- package/dist/tools/jules/cancel-session.js +1 -1
- package/dist/tools/jules/create-session.test.js +11 -11
- package/dist/tools/jules/get-messages.js +1 -1
- package/dist/tools/jules/get-messages.test.js +3 -3
- package/dist/tools/jules/get-pr.test.js +3 -3
- package/dist/tools/jules/list-sessions.js +1 -1
- package/dist/tools/jules/send-message.js +1 -1
- package/dist/tools/jules/send-message.test.js +5 -5
- package/dist/tools/jules/shared.js +4 -4
- package/dist/tools/jules/trigger-feedback.js +1 -1
- package/dist/tools/jules/trigger-feedback.test.js +5 -5
- package/dist/tools/repo.get_command_schema.js +2 -2
- package/dist/tools/repo.get_workflow_runs.js +1 -1
- package/package.json +1 -1
- package/scripts/sync-mcp-schemas.ts +2 -0
- package/src/config.test.ts +3 -3
- package/src/config.ts +1 -1
- package/src/lib/result.test.ts +4 -2
- package/src/lib/result.ts +6 -0
- package/src/lib/shell.ts +6 -6
- package/src/mcp/definitions.ts +76 -3
- package/src/mcp/http.test.ts +72 -0
- package/src/mcp/http.ts +118 -0
- package/src/mcp/server.test.ts +24 -0
- package/src/mcp/server.ts +63 -12
- package/src/tools/contract.ts +109 -1
- package/src/tools/github.checkout_branch.ts +1 -1
- package/src/tools/github.comment_triage_summary.ts +1 -1
- package/src/tools/github.create_issue.test.ts +6 -6
- package/src/tools/github.create_issue.ts +1 -1
- package/src/tools/github.create_pull_request.test.ts +4 -4
- package/src/tools/github.create_pull_request.ts +1 -1
- package/src/tools/github.get_pr.ts +1 -1
- package/src/tools/github.get_pr_commits.ts +1 -1
- package/src/tools/github.get_pr_diff.ts +1 -1
- package/src/tools/github.get_pr_reviews.test.ts +9 -9
- package/src/tools/github.get_pr_reviews.ts +1 -1
- package/src/tools/github.issue_comment.ts +1 -1
- package/src/tools/github.issue_update.ts +1 -1
- package/src/tools/github.issue_view.ts +1 -1
- package/src/tools/github.search_open_prs.ts +1 -1
- package/src/tools/jules/cancel-session.ts +1 -1
- package/src/tools/jules/create-session.test.ts +11 -11
- package/src/tools/jules/get-messages.test.ts +3 -3
- package/src/tools/jules/get-messages.ts +1 -1
- package/src/tools/jules/get-pr.test.ts +3 -3
- package/src/tools/jules/list-sessions.ts +1 -1
- package/src/tools/jules/send-message.test.ts +5 -5
- package/src/tools/jules/send-message.ts +1 -1
- package/src/tools/jules/shared.ts +4 -4
- package/src/tools/jules/trigger-feedback.test.ts +5 -5
- package/src/tools/jules/trigger-feedback.ts +1 -1
- package/src/tools/repo.get_command_schema.ts +2 -2
- package/src/tools/repo.get_workflow_runs.ts +1 -1
package/src/mcp/http.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
|
|
6
|
+
export interface HttpServerOptions {
|
|
7
|
+
port?: number;
|
|
8
|
+
bearerToken?: string;
|
|
9
|
+
mcpServer: Server;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createMcpHttpServer(options: HttpServerOptions) {
|
|
13
|
+
const {
|
|
14
|
+
port = 3000,
|
|
15
|
+
bearerToken = process.env.BOOMTICK_MCP_BEARER_TOKEN || process.env.MCP_BEARER_TOKEN || process.env.AUTH_TOKEN,
|
|
16
|
+
mcpServer,
|
|
17
|
+
} = options;
|
|
18
|
+
|
|
19
|
+
const transports = new Map<string, SSEServerTransport>();
|
|
20
|
+
|
|
21
|
+
const server = http.createServer(async (req, res) => {
|
|
22
|
+
// CORS Headers
|
|
23
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
24
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
25
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
26
|
+
|
|
27
|
+
if (req.method === "OPTIONS") {
|
|
28
|
+
res.writeHead(204);
|
|
29
|
+
res.end();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
34
|
+
|
|
35
|
+
// Endpoint 1: OAuth Protected Resource metadata
|
|
36
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && req.method === "GET") {
|
|
37
|
+
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
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Bearer Token Validation for SSE & Messages endpoints
|
|
51
|
+
if (url.pathname === "/sse" || url.pathname === "/messages") {
|
|
52
|
+
if (!bearerToken) {
|
|
53
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
54
|
+
res.end(JSON.stringify({ error: "Server misconfiguration: bearerToken is required for HTTP transport" }));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const authHeader = req.headers.authorization;
|
|
59
|
+
const expectedAuth = `Bearer ${bearerToken}`;
|
|
60
|
+
const authBuffer = Buffer.from(authHeader || "");
|
|
61
|
+
const expectedBuffer = Buffer.from(expectedAuth);
|
|
62
|
+
const isMatch =
|
|
63
|
+
Boolean(authHeader) &&
|
|
64
|
+
authBuffer.length === expectedBuffer.length &&
|
|
65
|
+
crypto.timingSafeEqual(authBuffer, expectedBuffer);
|
|
66
|
+
|
|
67
|
+
if (!isMatch) {
|
|
68
|
+
res.writeHead(401, {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token"',
|
|
71
|
+
});
|
|
72
|
+
res.end(JSON.stringify({ error: "unauthorized", message: "Invalid or missing Bearer token" }));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Endpoint 2: SSE stream setup
|
|
78
|
+
if (url.pathname === "/sse" && req.method === "GET") {
|
|
79
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
80
|
+
transports.set(transport.sessionId, transport);
|
|
81
|
+
transport.onclose = () => {
|
|
82
|
+
transports.delete(transport.sessionId);
|
|
83
|
+
};
|
|
84
|
+
await mcpServer.connect(transport);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Endpoint 3: POST messages
|
|
89
|
+
if (url.pathname === "/messages" && req.method === "POST") {
|
|
90
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
91
|
+
const transport = sessionId ? transports.get(sessionId) : null;
|
|
92
|
+
if (transport) {
|
|
93
|
+
await transport.handlePostMessage(req, res);
|
|
94
|
+
} else {
|
|
95
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
96
|
+
res.end(JSON.stringify({ error: "Session not found or expired" }));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
102
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
server,
|
|
107
|
+
listen: (customPort?: number) =>
|
|
108
|
+
new Promise<number>((resolve) => {
|
|
109
|
+
const p = customPort ?? port;
|
|
110
|
+
server.listen(p, () => {
|
|
111
|
+
const addr = server.address();
|
|
112
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : p;
|
|
113
|
+
console.error(`Boomtick MCP Server running on HTTP port ${actualPort}`);
|
|
114
|
+
resolve(actualPort);
|
|
115
|
+
});
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
}
|
package/src/mcp/server.test.ts
CHANGED
|
@@ -104,9 +104,32 @@ describe("BoomtickMCPServer", () => {
|
|
|
104
104
|
expect(result.resources).toBeDefined();
|
|
105
105
|
expect(Array.isArray(result.resources)).toBe(true);
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
it("should return resource templates list", async () => {
|
|
109
|
+
const handler = requestHandlers.get("resources/templates/list");
|
|
110
|
+
expect(handler).toBeDefined();
|
|
111
|
+
const result = await handler!({ method: "resources/templates/list" });
|
|
112
|
+
expect(result.resourceTemplates).toBeDefined();
|
|
113
|
+
expect(Array.isArray(result.resourceTemplates)).toBe(true);
|
|
114
|
+
});
|
|
107
115
|
});
|
|
108
116
|
|
|
109
117
|
describe("ReadResource", () => {
|
|
118
|
+
it("should handle mcp://workspace/agent-contract", async () => {
|
|
119
|
+
const handler = requestHandlers.get("resources/read");
|
|
120
|
+
vi.mocked(fs.readFile).mockResolvedValue("# Agent Contract\nRule 1");
|
|
121
|
+
|
|
122
|
+
const result = await handler!({
|
|
123
|
+
method: "resources/read",
|
|
124
|
+
params: { uri: "mcp://workspace/agent-contract" },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(fs.readFile).toHaveBeenCalledWith("/mock/repo/.agents/AGENT_CONTRACT.md", "utf-8");
|
|
128
|
+
expect(result).toEqual({
|
|
129
|
+
contents: [{ uri: "mcp://workspace/agent-contract", mimeType: "text/markdown", text: "# Agent Contract\nRule 1" }],
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
110
133
|
it("should handle repo://package-json", async () => {
|
|
111
134
|
const handler = requestHandlers.get("resources/read");
|
|
112
135
|
vi.mocked(fs.readFile).mockResolvedValue('{"name": "test"}');
|
|
@@ -196,6 +219,7 @@ describe("BoomtickMCPServer", () => {
|
|
|
196
219
|
|
|
197
220
|
expect(result).toEqual({
|
|
198
221
|
content: [{ type: "text", text: JSON.stringify({ status: "ok" }, null, 2) }],
|
|
222
|
+
structuredContent: { status: "ok" },
|
|
199
223
|
});
|
|
200
224
|
});
|
|
201
225
|
|
package/src/mcp/server.ts
CHANGED
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
CallToolRequestSchema,
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
6
|
ListResourcesRequestSchema,
|
|
7
|
+
ListResourceTemplatesRequestSchema,
|
|
7
8
|
ReadResourceRequestSchema,
|
|
8
9
|
ListPromptsRequestSchema,
|
|
9
10
|
GetPromptRequestSchema,
|
|
10
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
12
|
import { config } from "../config.js";
|
|
12
13
|
import { createSuccessResult, createErrorResult } from "../lib/result.js";
|
|
13
|
-
import { MCP_TOOLS, MCP_PROMPTS, MCP_RESOURCES } from "./definitions.js";
|
|
14
|
+
import { MCP_TOOLS, MCP_PROMPTS, MCP_RESOURCES, MCP_RESOURCE_TEMPLATES } from "./definitions.js";
|
|
14
15
|
import { normalizeArguments } from "../lib/param_utils.js";
|
|
15
16
|
import { healthHandler, HealthCheckInputSchema } from "./tools.js";
|
|
16
17
|
import { searchOpenPrsHandler, SearchOpenPrsInputSchema } from "../tools/github.search_open_prs.js";
|
|
@@ -52,6 +53,7 @@ import path from "path";
|
|
|
52
53
|
|
|
53
54
|
import { spawnSync } from "child_process";
|
|
54
55
|
import { fileURLToPath } from "url";
|
|
56
|
+
import { createMcpHttpServer } from "./http.js";
|
|
55
57
|
|
|
56
58
|
export class BoomtickMCPServer {
|
|
57
59
|
private server: Server;
|
|
@@ -117,29 +119,55 @@ export class BoomtickMCPServer {
|
|
|
117
119
|
};
|
|
118
120
|
});
|
|
119
121
|
|
|
122
|
+
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
123
|
+
return {
|
|
124
|
+
resourceTemplates: MCP_RESOURCE_TEMPLATES,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
120
128
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
121
129
|
const uri = request.params.uri;
|
|
122
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
uri === "mcp://workspace/agent-contract" ||
|
|
132
|
+
uri === "mcp://workspace/agent_contract" ||
|
|
133
|
+
uri === "repo://agent-contract"
|
|
134
|
+
) {
|
|
135
|
+
const contractPath = path.join(config.repoPath, ".agents", "AGENT_CONTRACT.md");
|
|
136
|
+
const content = await fs.readFile(contractPath, "utf-8");
|
|
137
|
+
return {
|
|
138
|
+
contents: [{ uri, mimeType: "text/markdown", text: content }],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
uri === "mcp://git/changed-files" ||
|
|
143
|
+
uri === "repo://changed-files"
|
|
144
|
+
) {
|
|
145
|
+
const changedFiles = await getChangedFilesHandler(GetChangedFilesInputSchema.parse({}));
|
|
146
|
+
return {
|
|
147
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(changedFiles, null, 2) }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (uri === "repo://package-json" || uri === "mcp://workspace/package-json") {
|
|
123
151
|
const content = await fs.readFile(path.join(config.repoPath, "package.json"), "utf-8");
|
|
124
152
|
return {
|
|
125
153
|
contents: [{ uri, mimeType: "application/json", text: content }],
|
|
126
154
|
};
|
|
127
155
|
}
|
|
128
|
-
if (uri === "repo://design-tokens") {
|
|
156
|
+
if (uri === "repo://design-tokens" || uri === "mcp://workspace/design-tokens") {
|
|
129
157
|
const tokensPath = path.join(config.repoPath, "src/styles/design-tokens.ts");
|
|
130
158
|
const content = await fs.readFile(tokensPath, "utf-8");
|
|
131
159
|
return {
|
|
132
160
|
contents: [{ uri, mimeType: "text/typescript", text: content }],
|
|
133
161
|
};
|
|
134
162
|
}
|
|
135
|
-
if (uri.startsWith("repo://diff/")) {
|
|
163
|
+
if (uri.startsWith("mcp://git/diff/") || uri.startsWith("repo://diff/")) {
|
|
136
164
|
const prNumber = parseInt(uri.split("/").pop() || "");
|
|
137
165
|
const diff = await getPrDiffHandler({ prNumber });
|
|
138
166
|
return {
|
|
139
167
|
contents: [{ uri, mimeType: "text/plain", text: diff.diffText }],
|
|
140
168
|
};
|
|
141
169
|
}
|
|
142
|
-
if (uri.startsWith("repo://ci/")) {
|
|
170
|
+
if (uri.startsWith("mcp://ci/logs/") || uri.startsWith("repo://ci/")) {
|
|
143
171
|
const prNumber = parseInt(uri.split("/").pop() || "");
|
|
144
172
|
const logs = await readCiLogsHandler({ prNumber });
|
|
145
173
|
return {
|
|
@@ -263,7 +291,7 @@ export class BoomtickMCPServer {
|
|
|
263
291
|
}
|
|
264
292
|
});
|
|
265
293
|
}
|
|
266
|
-
|
|
294
|
+
private prepareEnvironmentAndPreflight() {
|
|
267
295
|
const projectRoot = config.repoPath || process.cwd();
|
|
268
296
|
const homeDir = process.env.HOME || "";
|
|
269
297
|
const extraPaths = [];
|
|
@@ -315,9 +343,9 @@ export class BoomtickMCPServer {
|
|
|
315
343
|
// 3. SAFE REPOSITORY RESOLUTION
|
|
316
344
|
const repoPath = config.repoPath || projectRoot;
|
|
317
345
|
|
|
318
|
-
// 4. SECURE, UNIX-ONLY PRE-FLIGHT CHECK FOR TD
|
|
346
|
+
// 4. SECURE, UNIX-ONLY PRE-FLIGHT CHECK FOR TD
|
|
319
347
|
try {
|
|
320
|
-
const result = spawnSync("td
|
|
348
|
+
const result = spawnSync("td", ["doctor"], {
|
|
321
349
|
encoding: "utf-8",
|
|
322
350
|
cwd: repoPath,
|
|
323
351
|
shell: false // Explicitly disabled to clear Semgrep rules
|
|
@@ -331,16 +359,39 @@ export class BoomtickMCPServer {
|
|
|
331
359
|
console.error("status:", result.status);
|
|
332
360
|
console.error("stdout:", result.stdout);
|
|
333
361
|
console.error("stderr:", result.stderr);
|
|
334
|
-
throw new Error(`td
|
|
362
|
+
throw new Error(`td doctor returned exit code: ${result.status}`);
|
|
335
363
|
}
|
|
336
364
|
|
|
337
|
-
console.error("✅ td
|
|
365
|
+
console.error("✅ td verified on PATH securely");
|
|
338
366
|
} catch (error) {
|
|
339
|
-
console.error("❌ Fatal: td
|
|
367
|
+
console.error("❌ Fatal: td is not resolvable on PATH. MCP tools will fail.");
|
|
340
368
|
console.error("Please ensure the project is installed: pip install -e .");
|
|
341
|
-
console.error("❌ Fatal: td
|
|
369
|
+
console.error("❌ Fatal: td pre-flight check failed:", error instanceof Error ? error.message : String(error));
|
|
342
370
|
process.exit(1);
|
|
343
371
|
}
|
|
372
|
+
}
|
|
373
|
+
|
|
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
|
+
async run() {
|
|
381
|
+
this.prepareEnvironmentAndPreflight();
|
|
382
|
+
|
|
383
|
+
const isHttp =
|
|
384
|
+
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"));
|
|
388
|
+
|
|
389
|
+
if (isHttp) {
|
|
390
|
+
const port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
|
|
391
|
+
const mcpHttp = createMcpHttpServer({ port, mcpServer: this.server });
|
|
392
|
+
await mcpHttp.listen(port);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
344
395
|
|
|
345
396
|
const transport = new StdioServerTransport();
|
|
346
397
|
await this.server.connect(transport);
|
package/src/tools/contract.ts
CHANGED
|
@@ -847,6 +847,30 @@ export const CodeReviewResultJsonSchema = {
|
|
|
847
847
|
"default": null,
|
|
848
848
|
"title": "Line"
|
|
849
849
|
},
|
|
850
|
+
"startLine": {
|
|
851
|
+
"anyOf": [
|
|
852
|
+
{
|
|
853
|
+
"type": "integer"
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
"type": "null"
|
|
857
|
+
}
|
|
858
|
+
],
|
|
859
|
+
"default": null,
|
|
860
|
+
"title": "Startline"
|
|
861
|
+
},
|
|
862
|
+
"endLine": {
|
|
863
|
+
"anyOf": [
|
|
864
|
+
{
|
|
865
|
+
"type": "integer"
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
"type": "null"
|
|
869
|
+
}
|
|
870
|
+
],
|
|
871
|
+
"default": null,
|
|
872
|
+
"title": "Endline"
|
|
873
|
+
},
|
|
850
874
|
"snippet": {
|
|
851
875
|
"anyOf": [
|
|
852
876
|
{
|
|
@@ -882,6 +906,18 @@ export const CodeReviewResultJsonSchema = {
|
|
|
882
906
|
],
|
|
883
907
|
"default": null,
|
|
884
908
|
"title": "Counterexample"
|
|
909
|
+
},
|
|
910
|
+
"suggestion": {
|
|
911
|
+
"anyOf": [
|
|
912
|
+
{
|
|
913
|
+
"type": "string"
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
"type": "null"
|
|
917
|
+
}
|
|
918
|
+
],
|
|
919
|
+
"default": null,
|
|
920
|
+
"title": "Suggestion"
|
|
885
921
|
}
|
|
886
922
|
},
|
|
887
923
|
"required": [
|
|
@@ -1077,6 +1113,30 @@ export const CodeReviewStateJsonSchema = {
|
|
|
1077
1113
|
"default": null,
|
|
1078
1114
|
"title": "Line"
|
|
1079
1115
|
},
|
|
1116
|
+
"startLine": {
|
|
1117
|
+
"anyOf": [
|
|
1118
|
+
{
|
|
1119
|
+
"type": "integer"
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
"type": "null"
|
|
1123
|
+
}
|
|
1124
|
+
],
|
|
1125
|
+
"default": null,
|
|
1126
|
+
"title": "Startline"
|
|
1127
|
+
},
|
|
1128
|
+
"endLine": {
|
|
1129
|
+
"anyOf": [
|
|
1130
|
+
{
|
|
1131
|
+
"type": "integer"
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
"type": "null"
|
|
1135
|
+
}
|
|
1136
|
+
],
|
|
1137
|
+
"default": null,
|
|
1138
|
+
"title": "Endline"
|
|
1139
|
+
},
|
|
1080
1140
|
"snippet": {
|
|
1081
1141
|
"anyOf": [
|
|
1082
1142
|
{
|
|
@@ -1112,6 +1172,18 @@ export const CodeReviewStateJsonSchema = {
|
|
|
1112
1172
|
],
|
|
1113
1173
|
"default": null,
|
|
1114
1174
|
"title": "Counterexample"
|
|
1175
|
+
},
|
|
1176
|
+
"suggestion": {
|
|
1177
|
+
"anyOf": [
|
|
1178
|
+
{
|
|
1179
|
+
"type": "string"
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
"type": "null"
|
|
1183
|
+
}
|
|
1184
|
+
],
|
|
1185
|
+
"default": null,
|
|
1186
|
+
"title": "Suggestion"
|
|
1115
1187
|
}
|
|
1116
1188
|
},
|
|
1117
1189
|
"required": [
|
|
@@ -2760,7 +2832,7 @@ export const ReviewCommentJsonSchema = {
|
|
|
2760
2832
|
"type": "object"
|
|
2761
2833
|
} as const;
|
|
2762
2834
|
|
|
2763
|
-
export const ReviewFindingSchema = z.object({ "id": z.string(), "file": z.string(), "issue": z.string(), "status": z.enum(["open","resolved"]), "confidence": z.enum(["high","medium","low"]), "severity": z.union([z.enum(["HIGH","MEDIUM","LOW"]), z.null()]).default(null), "line": z.union([z.number().int(), z.null()]).default(null), "snippet": z.union([z.string(), z.null()]).default(null), "fixSummary": z.union([z.string(), z.null()]).default(null), "counterexample": z.union([z.string(), z.null()]).default(null) });
|
|
2835
|
+
export const ReviewFindingSchema = z.object({ "id": z.string(), "file": z.string(), "issue": z.string(), "status": z.enum(["open","resolved"]), "confidence": z.enum(["high","medium","low"]), "severity": z.union([z.enum(["HIGH","MEDIUM","LOW"]), z.null()]).default(null), "line": z.union([z.number().int(), z.null()]).default(null), "startLine": z.union([z.number().int(), z.null()]).default(null), "endLine": z.union([z.number().int(), z.null()]).default(null), "snippet": z.union([z.string(), z.null()]).default(null), "fixSummary": z.union([z.string(), z.null()]).default(null), "counterexample": z.union([z.string(), z.null()]).default(null), "suggestion": z.union([z.string(), z.null()]).default(null) });
|
|
2764
2836
|
|
|
2765
2837
|
export type ReviewFinding = z.infer<typeof ReviewFindingSchema>;
|
|
2766
2838
|
|
|
@@ -2824,6 +2896,30 @@ export const ReviewFindingJsonSchema = {
|
|
|
2824
2896
|
"default": null,
|
|
2825
2897
|
"title": "Line"
|
|
2826
2898
|
},
|
|
2899
|
+
"startLine": {
|
|
2900
|
+
"anyOf": [
|
|
2901
|
+
{
|
|
2902
|
+
"type": "integer"
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
"type": "null"
|
|
2906
|
+
}
|
|
2907
|
+
],
|
|
2908
|
+
"default": null,
|
|
2909
|
+
"title": "Startline"
|
|
2910
|
+
},
|
|
2911
|
+
"endLine": {
|
|
2912
|
+
"anyOf": [
|
|
2913
|
+
{
|
|
2914
|
+
"type": "integer"
|
|
2915
|
+
},
|
|
2916
|
+
{
|
|
2917
|
+
"type": "null"
|
|
2918
|
+
}
|
|
2919
|
+
],
|
|
2920
|
+
"default": null,
|
|
2921
|
+
"title": "Endline"
|
|
2922
|
+
},
|
|
2827
2923
|
"snippet": {
|
|
2828
2924
|
"anyOf": [
|
|
2829
2925
|
{
|
|
@@ -2859,6 +2955,18 @@ export const ReviewFindingJsonSchema = {
|
|
|
2859
2955
|
],
|
|
2860
2956
|
"default": null,
|
|
2861
2957
|
"title": "Counterexample"
|
|
2958
|
+
},
|
|
2959
|
+
"suggestion": {
|
|
2960
|
+
"anyOf": [
|
|
2961
|
+
{
|
|
2962
|
+
"type": "string"
|
|
2963
|
+
},
|
|
2964
|
+
{
|
|
2965
|
+
"type": "null"
|
|
2966
|
+
}
|
|
2967
|
+
],
|
|
2968
|
+
"default": null,
|
|
2969
|
+
"title": "Suggestion"
|
|
2862
2970
|
}
|
|
2863
2971
|
},
|
|
2864
2972
|
"required": [
|
|
@@ -7,7 +7,7 @@ export const CheckoutBranchInputSchema = z.object({
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
export async function checkoutBranchHandler(args: z.infer<typeof CheckoutBranchInputSchema>) {
|
|
10
|
-
const result = await runCommand("td
|
|
10
|
+
const result = await runCommand("td", ["gh", "checkout", args.branch], { cwd: args.worktreePath });
|
|
11
11
|
|
|
12
12
|
if (result.exitCode !== 0) {
|
|
13
13
|
throw new Error(`Failed to checkout branch ${args.branch}: ${result.stderr}`);
|
|
@@ -7,7 +7,7 @@ export const CommentTriageSummaryInputSchema = z.object({
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
export async function commentTriageSummaryHandler(args: z.infer<typeof CommentTriageSummaryInputSchema>) {
|
|
10
|
-
const result = await runCommand("td
|
|
10
|
+
const result = await runCommand("td", [
|
|
11
11
|
"gh",
|
|
12
12
|
"post-comment",
|
|
13
13
|
"--pr", args.prNumber.toString(),
|
|
@@ -23,11 +23,11 @@ describe("github.create_issue", () => {
|
|
|
23
23
|
stderr: "",
|
|
24
24
|
exitCode: 0,
|
|
25
25
|
durationMs: 10,
|
|
26
|
-
command: "td
|
|
26
|
+
command: "td gh create-issue --title 'Test Issue' --body 'Test Body'"
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const result = await createIssueHandler({ title: "Test Issue", body: "Test Body", file: null, repo: null });
|
|
30
|
-
expect(shell.runCommand).toHaveBeenCalledWith("td
|
|
30
|
+
expect(shell.runCommand).toHaveBeenCalledWith("td", [
|
|
31
31
|
"gh",
|
|
32
32
|
"create-issue",
|
|
33
33
|
"--title",
|
|
@@ -58,7 +58,7 @@ describe("github.create_issue", () => {
|
|
|
58
58
|
stderr: "",
|
|
59
59
|
exitCode: 0,
|
|
60
60
|
durationMs: 10,
|
|
61
|
-
command: "td
|
|
61
|
+
command: "td gh create-issue --title 'Cross Repo Issue' --body 'Body' --repo 'org/other-repo'"
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
const result = await createIssueHandler({
|
|
@@ -68,7 +68,7 @@ describe("github.create_issue", () => {
|
|
|
68
68
|
repo: "org/other-repo"
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
expect(shell.runCommand).toHaveBeenCalledWith("td
|
|
71
|
+
expect(shell.runCommand).toHaveBeenCalledWith("td", [
|
|
72
72
|
"gh",
|
|
73
73
|
"create-issue",
|
|
74
74
|
"--title",
|
|
@@ -88,7 +88,7 @@ describe("github.create_issue", () => {
|
|
|
88
88
|
stderr: "Auth failed",
|
|
89
89
|
exitCode: 1,
|
|
90
90
|
durationMs: 10,
|
|
91
|
-
command: "td
|
|
91
|
+
command: "td gh create-issue"
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null, repo: null })).rejects.toThrow("Failed to create issue: Auth failed");
|
|
@@ -105,7 +105,7 @@ describe("github.create_issue", () => {
|
|
|
105
105
|
stderr: "",
|
|
106
106
|
exitCode: 0,
|
|
107
107
|
durationMs: 10,
|
|
108
|
-
command: "td
|
|
108
|
+
command: "td gh create-issue"
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null, repo: null })).rejects.toThrow("Failed to create issue: Repo not found");
|
|
@@ -15,7 +15,7 @@ export async function createIssueHandler(args: z.input<typeof CreateIssueInputSc
|
|
|
15
15
|
if (params.file) cmdArgs.push("--file", params.file);
|
|
16
16
|
cmdArgs.push("--repo", targetRepo);
|
|
17
17
|
|
|
18
|
-
const result = await runCommand("td
|
|
18
|
+
const result = await runCommand("td", cmdArgs);
|
|
19
19
|
|
|
20
20
|
if (result.exitCode !== 0) {
|
|
21
21
|
throw new Error(`Failed to create issue: ${sanitizeError(result.stderr)}`);
|
|
@@ -21,7 +21,7 @@ describe("github.create_pull_request", () => {
|
|
|
21
21
|
stderr: "",
|
|
22
22
|
exitCode: 0,
|
|
23
23
|
durationMs: 10,
|
|
24
|
-
command: "td
|
|
24
|
+
command: "td gh create-pr --title 'PR' --body 'Body' --head 'feature' --base 'main'"
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const result = await createPullRequestHandler({
|
|
@@ -50,7 +50,7 @@ describe("github.create_pull_request", () => {
|
|
|
50
50
|
stderr: "",
|
|
51
51
|
exitCode: 0,
|
|
52
52
|
durationMs: 10,
|
|
53
|
-
command: "td
|
|
53
|
+
command: "td gh create-pr --title 'PR' --body 'Body' --head 'feature' --base 'main' --repo 'org/other-repo'"
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
const result = await createPullRequestHandler({
|
|
@@ -62,7 +62,7 @@ describe("github.create_pull_request", () => {
|
|
|
62
62
|
repo: "org/other-repo"
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
expect(shell.runCommand).toHaveBeenCalledWith("td
|
|
65
|
+
expect(shell.runCommand).toHaveBeenCalledWith("td", [
|
|
66
66
|
"gh",
|
|
67
67
|
"create-pr",
|
|
68
68
|
"--title",
|
|
@@ -91,7 +91,7 @@ describe("github.create_pull_request", () => {
|
|
|
91
91
|
stderr: "",
|
|
92
92
|
exitCode: 0,
|
|
93
93
|
durationMs: 10,
|
|
94
|
-
command: "td
|
|
94
|
+
command: "td gh create-pr"
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
await expect(createPullRequestHandler({
|
|
@@ -23,7 +23,7 @@ export async function createPullRequestHandler(args: z.input<typeof CreatePullRe
|
|
|
23
23
|
tdArgs.push("--draft");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const result = await runCommand("td
|
|
26
|
+
const result = await runCommand("td", tdArgs);
|
|
27
27
|
|
|
28
28
|
if (result.exitCode !== 0) {
|
|
29
29
|
// Attempt to parse stdout as JSON to see if there's a structured error message
|
|
@@ -12,7 +12,7 @@ export async function getPrHandler(args: z.infer<typeof GetPrInputSchema>) {
|
|
|
12
12
|
const params = GetPrInputSchema.parse(args);
|
|
13
13
|
const targetRepo = resolveRepo(params.repo);
|
|
14
14
|
|
|
15
|
-
const result = await runCommand("td
|
|
15
|
+
const result = await runCommand("td", ["gh", "view", params.prNumber.toString(), "--repo", targetRepo]);
|
|
16
16
|
|
|
17
17
|
if (result.exitCode !== 0) {
|
|
18
18
|
throw new Error(`Failed to get PR details: ${sanitizeError(result.stderr)}`);
|
|
@@ -12,7 +12,7 @@ export async function getPrCommitsHandler(args: z.infer<typeof GetPrCommitsInput
|
|
|
12
12
|
const params = GetPrCommitsInputSchema.parse(args);
|
|
13
13
|
const targetRepo = resolveRepo(params.repo);
|
|
14
14
|
|
|
15
|
-
const result = await runCommand("td
|
|
15
|
+
const result = await runCommand("td", ["gh", "list-pr-commits", "--pr-number", params.prNumber.toString(), "--repo", targetRepo]);
|
|
16
16
|
|
|
17
17
|
if (result.exitCode !== 0) {
|
|
18
18
|
throw new Error(`Failed to list PR commits: ${sanitizeError(result.stderr)}`);
|
|
@@ -11,7 +11,7 @@ export async function getPrDiffHandler(args: z.infer<typeof GetPrDiffInputSchema
|
|
|
11
11
|
const params = GetPrDiffInputSchema.parse(args);
|
|
12
12
|
const targetRepo = resolveRepo(params.repo);
|
|
13
13
|
|
|
14
|
-
const result = await runCommand("td
|
|
14
|
+
const result = await runCommand("td", [
|
|
15
15
|
"gh", "pr-diff", params.prNumber.toString(), "--repo", targetRepo
|
|
16
16
|
]);
|
|
17
17
|
|