@arii/boomtick-mcp 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp/schemas/github.create_issue.json +72 -3
- package/.mcp/schemas/github.get_merge_conflict_files.json +40 -3
- package/.mcp/schemas/github.get_pr_diff.json +30 -3
- package/.mcp/schemas/github.issue_update.json +72 -3
- package/.mcp/schemas/github.search_open_prs.json +130 -3
- package/.mcp/schemas/github_create_issue.json +72 -3
- package/.mcp/schemas/github_get_merge_conflict_files.json +40 -3
- package/.mcp/schemas/github_get_pr_diff.json +30 -3
- package/.mcp/schemas/github_issue_update.json +72 -3
- package/.mcp/schemas/github_search_open_prs.json +130 -3
- package/.mcp/schemas/repo.check_jules_status.json +38 -0
- package/.mcp/schemas/repo.run_tests.json +31 -3
- package/.mcp/schemas/repo_check_jules_status.json +38 -0
- package/.mcp/schemas/repo_run_tests.json +31 -3
- package/CHANGELOG.md +8 -0
- package/README.md +22 -1
- package/actions/run-project-gate/action.yml +1 -1
- package/dist/config/dynamic.js +33 -0
- package/dist/config/github.js +16 -0
- package/dist/config/repo.js +28 -0
- package/dist/config.js +16 -81
- package/dist/http.js +10 -0
- package/dist/mcp/definitions.js +29 -7
- package/dist/mcp/http.js +18 -10
- package/dist/mcp/http.test.js +54 -4
- package/dist/mcp/server.js +35 -10
- package/dist/mcp/server.test.js +28 -1
- package/dist/tools/contract.js +77 -0
- package/dist/tools/github.comment_triage_summary.js +3 -10
- package/dist/tools/github.get_merge_conflict_files.js +4 -2
- package/dist/tools/github.get_pr_diff.js +3 -11
- package/dist/tools/jules/cancel-session.js +2 -9
- package/dist/tools/jules/cli.js +31 -0
- package/dist/tools/jules/get-messages.js +2 -9
- package/dist/tools/jules/list-sessions.js +4 -35
- package/dist/tools/jules/send-message.js +2 -9
- package/dist/tools/jules/session.js +26 -0
- package/dist/tools/jules/shared.js +2 -50
- package/dist/tools/jules/trigger-feedback.js +2 -10
- package/dist/tools/repo.check_jules_status.js +148 -0
- package/dist/tools/repo.check_jules_status.test.js +47 -0
- package/dist/tools/repo.run_tests.js +13 -1
- package/package.json +3 -2
- package/scripts/sync-contracts.ts +11 -2
- package/src/config/dynamic.ts +36 -0
- package/src/config/github.ts +16 -0
- package/src/config/repo.ts +36 -0
- package/src/config.ts +16 -89
- package/src/http.ts +13 -0
- package/src/mcp/definitions.ts +34 -6
- package/src/mcp/http.test.ts +65 -5
- package/src/mcp/http.ts +20 -14
- package/src/mcp/server.test.ts +34 -1
- package/src/mcp/server.ts +36 -11
- package/src/tools/contract.ts +85 -0
- package/src/tools/github.comment_triage_summary.ts +3 -12
- package/src/tools/github.get_merge_conflict_files.ts +5 -2
- package/src/tools/github.get_pr_diff.ts +3 -13
- package/src/tools/jules/cancel-session.ts +2 -11
- package/src/tools/jules/cli.ts +42 -0
- package/src/tools/jules/get-messages.ts +2 -11
- package/src/tools/jules/list-sessions.ts +5 -39
- package/src/tools/jules/send-message.ts +2 -11
- package/src/tools/jules/session.ts +28 -0
- package/src/tools/jules/shared.ts +2 -63
- package/src/tools/jules/trigger-feedback.ts +2 -13
- package/src/tools/repo.check_jules_status.test.ts +55 -0
- package/src/tools/repo.check_jules_status.ts +176 -0
- package/src/tools/repo.run_tests.ts +14 -1
package/src/mcp/definitions.ts
CHANGED
|
@@ -2,9 +2,12 @@ import { Tool, Prompt, Resource, ResourceTemplate } from "@modelcontextprotocol/
|
|
|
2
2
|
import {
|
|
3
3
|
HealthCheckInputJsonSchema,
|
|
4
4
|
SearchPRsInputJsonSchema,
|
|
5
|
+
SearchPRsResponseJsonSchema,
|
|
5
6
|
GetPrDiffInputJsonSchema,
|
|
7
|
+
PrDiffResponseJsonSchema,
|
|
6
8
|
CheckoutBranchInputJsonSchema,
|
|
7
9
|
GetMergeConflictFilesInputJsonSchema,
|
|
10
|
+
GetMergeConflictFilesResponseJsonSchema,
|
|
8
11
|
GetChangedFilesInputJsonSchema,
|
|
9
12
|
GetPackageScriptsInputJsonSchema,
|
|
10
13
|
ReadCiLogsInputJsonSchema,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
CreateBranchInputJsonSchema,
|
|
13
16
|
CreateRepairBranchInputJsonSchema,
|
|
14
17
|
RunTestsInputJsonSchema,
|
|
18
|
+
RunTestsResponseJsonSchema,
|
|
15
19
|
CommitPatchInputJsonSchema,
|
|
16
20
|
OpenReplacementPrInputJsonSchema,
|
|
17
21
|
CreatePullRequestInputJsonSchema,
|
|
@@ -19,8 +23,10 @@ import {
|
|
|
19
23
|
GetPrInputJsonSchema,
|
|
20
24
|
IssueViewInputJsonSchema,
|
|
21
25
|
IssueUpdateInputJsonSchema,
|
|
26
|
+
IssueUpdateResponseJsonSchema,
|
|
22
27
|
IssueCommentInputJsonSchema,
|
|
23
28
|
CreateIssueInputJsonSchema,
|
|
29
|
+
CreateIssueResponseJsonSchema,
|
|
24
30
|
GetCommandSchemaInputJsonSchema,
|
|
25
31
|
ReadAgentContextInputJsonSchema as ReadAgentContextSchema,
|
|
26
32
|
CreateJulesSessionInputJsonSchema,
|
|
@@ -78,6 +84,12 @@ export const MCP_RESOURCES: Resource[] = [
|
|
|
78
84
|
mimeType: "application/json",
|
|
79
85
|
description: "List of changed files in the repository.",
|
|
80
86
|
},
|
|
87
|
+
{
|
|
88
|
+
uri: "mcp://ci/latest-run",
|
|
89
|
+
name: "Latest CI Run",
|
|
90
|
+
mimeType: "application/json",
|
|
91
|
+
description: "Recent CI exit statuses, workflow runs, and annotations.",
|
|
92
|
+
},
|
|
81
93
|
{
|
|
82
94
|
uri: "repo://package-json",
|
|
83
95
|
name: "package.json",
|
|
@@ -130,7 +142,7 @@ const BASE_MCP_TOOLS: Tool[] = [
|
|
|
130
142
|
name: "github.search_open_prs",
|
|
131
143
|
description: "Search for open pull requests in the repository.",
|
|
132
144
|
inputSchema: SearchPRsInputJsonSchema as any,
|
|
133
|
-
outputSchema:
|
|
145
|
+
outputSchema: SearchPRsResponseJsonSchema as any,
|
|
134
146
|
},
|
|
135
147
|
{
|
|
136
148
|
name: "repo.read_agent_context",
|
|
@@ -142,7 +154,7 @@ const BASE_MCP_TOOLS: Tool[] = [
|
|
|
142
154
|
name: "github.get_pr_diff",
|
|
143
155
|
description: "Get the diff and changed files for a pull request.",
|
|
144
156
|
inputSchema: GetPrDiffInputJsonSchema as any,
|
|
145
|
-
outputSchema:
|
|
157
|
+
outputSchema: PrDiffResponseJsonSchema as any,
|
|
146
158
|
},
|
|
147
159
|
{
|
|
148
160
|
name: "github.checkout_branch",
|
|
@@ -154,7 +166,7 @@ const BASE_MCP_TOOLS: Tool[] = [
|
|
|
154
166
|
name: "github.get_merge_conflict_files",
|
|
155
167
|
description: "Detect files that conflict when a PR is merged with the base branch.",
|
|
156
168
|
inputSchema: GetMergeConflictFilesInputJsonSchema as any,
|
|
157
|
-
outputSchema:
|
|
169
|
+
outputSchema: GetMergeConflictFilesResponseJsonSchema as any,
|
|
158
170
|
},
|
|
159
171
|
{
|
|
160
172
|
name: "repo.get_changed_files",
|
|
@@ -198,11 +210,27 @@ const BASE_MCP_TOOLS: Tool[] = [
|
|
|
198
210
|
inputSchema: CreateRepairBranchInputJsonSchema as any,
|
|
199
211
|
outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
|
|
200
212
|
},
|
|
213
|
+
{
|
|
214
|
+
name: "repo.check_jules_status",
|
|
215
|
+
description: "Check or poll the status of a Jules remediation session and verify its branch or PR status.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
sessionId: { type: "string", description: "The unique ID or task ID of the Jules session." },
|
|
220
|
+
commitSha: { type: "string", description: "Commit SHA for idempotency key verification." },
|
|
221
|
+
failureLog: { type: "string", description: "Failure log content for idempotency key calculation." },
|
|
222
|
+
poll: { type: "boolean", description: "Whether to poll until session completion or timeout." },
|
|
223
|
+
timeoutMs: { type: "integer", description: "Timeout in milliseconds when polling." },
|
|
224
|
+
intervalMs: { type: "integer", description: "Polling interval in milliseconds." },
|
|
225
|
+
},
|
|
226
|
+
} as any,
|
|
227
|
+
outputSchema: DEFAULT_OUTPUT_SCHEMA as any,
|
|
228
|
+
},
|
|
201
229
|
{
|
|
202
230
|
name: "repo.run_tests",
|
|
203
231
|
description: "Run repository tests and checks.",
|
|
204
232
|
inputSchema: RunTestsInputJsonSchema as any,
|
|
205
|
-
outputSchema:
|
|
233
|
+
outputSchema: RunTestsResponseJsonSchema as any,
|
|
206
234
|
},
|
|
207
235
|
{
|
|
208
236
|
name: "repo.commit_patch",
|
|
@@ -262,7 +290,7 @@ const BASE_MCP_TOOLS: Tool[] = [
|
|
|
262
290
|
name: "github.issue_update",
|
|
263
291
|
description: "Update a GitHub issue's body, labels, and/or state.",
|
|
264
292
|
inputSchema: IssueUpdateInputJsonSchema as any,
|
|
265
|
-
outputSchema:
|
|
293
|
+
outputSchema: IssueUpdateResponseJsonSchema as any,
|
|
266
294
|
},
|
|
267
295
|
{
|
|
268
296
|
name: "github.issue_comment",
|
|
@@ -274,7 +302,7 @@ const BASE_MCP_TOOLS: Tool[] = [
|
|
|
274
302
|
name: "github.create_issue",
|
|
275
303
|
description: "Create a new GitHub issue.",
|
|
276
304
|
inputSchema: CreateIssueInputJsonSchema as any,
|
|
277
|
-
outputSchema:
|
|
305
|
+
outputSchema: CreateIssueResponseJsonSchema as any,
|
|
278
306
|
},
|
|
279
307
|
{
|
|
280
308
|
name: "jules.create_session",
|
package/src/mcp/http.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import http from "node:http";
|
|
3
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
3
|
import { createMcpHttpServer } from "./http.js";
|
|
4
|
+
import { BoomtickMCPServer } from "./server.js";
|
|
5
5
|
|
|
6
6
|
describe("createMcpHttpServer", () => {
|
|
7
7
|
let mcpServer: Server;
|
|
@@ -18,19 +18,48 @@ describe("createMcpHttpServer", () => {
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it("should respond with OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
|
|
21
|
+
it("should respond with RFC 9728 OAuth protected resource metadata on /.well-known/oauth-protected-resource", async () => {
|
|
22
22
|
httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
|
|
23
23
|
port = await httpServerObj.listen(0);
|
|
24
24
|
|
|
25
25
|
const res = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`);
|
|
26
26
|
expect(res.status).toBe(200);
|
|
27
|
+
expect(res.headers.get("content-type")).toContain("application/json");
|
|
28
|
+
|
|
27
29
|
const body = await res.json();
|
|
28
30
|
expect(body.resource).toBe("mcp://boomtick-mcp");
|
|
29
|
-
expect(body.authorization_servers).
|
|
31
|
+
expect(body.authorization_servers).toEqual(["https://auth.boomtick.dev"]);
|
|
30
32
|
expect(body.scopes_supported).toEqual(["mcp:read", "mcp:write"]);
|
|
33
|
+
expect(body.bearer_methods_supported).toEqual(["header"]);
|
|
34
|
+
expect(body.resource_documentation).toBe("https://github.com/arii/boomtick");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should handle HEAD requests for /.well-known/oauth-protected-resource", async () => {
|
|
38
|
+
httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
|
|
39
|
+
port = await httpServerObj.listen(0);
|
|
40
|
+
|
|
41
|
+
const res = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`, {
|
|
42
|
+
method: "HEAD",
|
|
43
|
+
});
|
|
44
|
+
expect(res.status).toBe(200);
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
expect(text).toBe("");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle OPTIONS preflight CORS requests", async () => {
|
|
50
|
+
httpServerObj = createMcpHttpServer({ port: 0, mcpServer });
|
|
51
|
+
port = await httpServerObj.listen(0);
|
|
52
|
+
|
|
53
|
+
const res = await fetch(`http://localhost:${port}/sse`, {
|
|
54
|
+
method: "OPTIONS",
|
|
55
|
+
});
|
|
56
|
+
expect(res.status).toBe(204);
|
|
57
|
+
expect(res.headers.get("access-control-allow-origin")).toBe("*");
|
|
58
|
+
expect(res.headers.get("access-control-allow-methods")).toContain("GET");
|
|
59
|
+
expect(res.headers.get("access-control-allow-headers")).toContain("Authorization");
|
|
31
60
|
});
|
|
32
61
|
|
|
33
|
-
it("should reject requests when Bearer token is missing or invalid", async () => {
|
|
62
|
+
it("should reject protected requests when Bearer token is missing or invalid", async () => {
|
|
34
63
|
httpServerObj = createMcpHttpServer({ port: 0, bearerToken: "secret123", mcpServer });
|
|
35
64
|
port = await httpServerObj.listen(0);
|
|
36
65
|
|
|
@@ -38,6 +67,7 @@ describe("createMcpHttpServer", () => {
|
|
|
38
67
|
const res1 = await fetch(`http://localhost:${port}/sse`);
|
|
39
68
|
expect(res1.status).toBe(401);
|
|
40
69
|
expect(res1.headers.get("www-authenticate")).toContain('Bearer realm="boomtick-mcp"');
|
|
70
|
+
expect(res1.headers.get("www-authenticate")).toContain('error="invalid_token"');
|
|
41
71
|
|
|
42
72
|
// Request with wrong token
|
|
43
73
|
const res2 = await fetch(`http://localhost:${port}/sse`, {
|
|
@@ -45,7 +75,7 @@ describe("createMcpHttpServer", () => {
|
|
|
45
75
|
});
|
|
46
76
|
expect(res2.status).toBe(401);
|
|
47
77
|
|
|
48
|
-
//
|
|
78
|
+
// Public resource metadata accessible with or without token
|
|
49
79
|
const res3 = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`, {
|
|
50
80
|
headers: { Authorization: "Bearer secret123" },
|
|
51
81
|
});
|
|
@@ -69,4 +99,34 @@ describe("createMcpHttpServer", () => {
|
|
|
69
99
|
const body = await res.json();
|
|
70
100
|
expect(body.error).toContain("Server misconfiguration");
|
|
71
101
|
});
|
|
102
|
+
|
|
103
|
+
it("should return 400 for message requests without session or invalid session", async () => {
|
|
104
|
+
httpServerObj = createMcpHttpServer({ port: 0, bearerToken: "secret123", mcpServer });
|
|
105
|
+
port = await httpServerObj.listen(0);
|
|
106
|
+
|
|
107
|
+
const res = await fetch(`http://localhost:${port}/messages?sessionId=invalid_session`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
Authorization: "Bearer secret123",
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(res.status).toBe(400);
|
|
117
|
+
const body = await res.json();
|
|
118
|
+
expect(body.error).toContain("Session not found or expired");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should allow BoomtickMCPServer to start HTTP server via run() when transport is http", async () => {
|
|
122
|
+
process.env.MCP_TRANSPORT = "http";
|
|
123
|
+
process.env.PORT = "0";
|
|
124
|
+
process.env.BOOMTICK_MCP_BEARER_TOKEN = "test-token";
|
|
125
|
+
|
|
126
|
+
const boomtickServer = new BoomtickMCPServer();
|
|
127
|
+
await boomtickServer.run();
|
|
128
|
+
|
|
129
|
+
// Verify instance initialized properly
|
|
130
|
+
expect(boomtickServer).toBeInstanceOf(BoomtickMCPServer);
|
|
131
|
+
});
|
|
72
132
|
});
|
package/src/mcp/http.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function createMcpHttpServer(options: HttpServerOptions) {
|
|
|
21
21
|
const server = http.createServer(async (req, res) => {
|
|
22
22
|
// CORS Headers
|
|
23
23
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
24
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
24
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD");
|
|
25
25
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
26
26
|
|
|
27
27
|
if (req.method === "OPTIONS") {
|
|
@@ -32,22 +32,28 @@ export function createMcpHttpServer(options: HttpServerOptions) {
|
|
|
32
32
|
|
|
33
33
|
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
34
34
|
|
|
35
|
-
// Endpoint 1: OAuth Protected Resource
|
|
36
|
-
if (url.pathname === "/.well-known/oauth-protected-resource" && req.method === "GET") {
|
|
35
|
+
// Endpoint 1: RFC 9728 OAuth Protected Resource Metadata Discovery
|
|
36
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && (req.method === "GET" || req.method === "HEAD")) {
|
|
37
|
+
const metadata = {
|
|
38
|
+
resource: process.env.MCP_RESOURCE_URI || process.env.BOOMTICK_RESOURCE_URI || "mcp://boomtick-mcp",
|
|
39
|
+
authorization_servers: [
|
|
40
|
+
process.env.MCP_OAUTH_AUTH_SERVER || "https://auth.boomtick.dev",
|
|
41
|
+
],
|
|
42
|
+
scopes_supported: ["mcp:read", "mcp:write"],
|
|
43
|
+
bearer_methods_supported: ["header"],
|
|
44
|
+
resource_documentation: process.env.MCP_RESOURCE_DOCS || "https://github.com/arii/boomtick",
|
|
45
|
+
};
|
|
46
|
+
|
|
37
47
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
],
|
|
44
|
-
scopes_supported: ["mcp:read", "mcp:write"],
|
|
45
|
-
})
|
|
46
|
-
);
|
|
48
|
+
if (req.method === "HEAD") {
|
|
49
|
+
res.end();
|
|
50
|
+
} else {
|
|
51
|
+
res.end(JSON.stringify(metadata, null, 2));
|
|
52
|
+
}
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
// Bearer Token Validation for
|
|
56
|
+
// Bearer Token / OAuth 2.1 Validation for Protected Routes (/sse & /messages)
|
|
51
57
|
if (url.pathname === "/sse" || url.pathname === "/messages") {
|
|
52
58
|
if (!bearerToken) {
|
|
53
59
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -67,7 +73,7 @@ export function createMcpHttpServer(options: HttpServerOptions) {
|
|
|
67
73
|
if (!isMatch) {
|
|
68
74
|
res.writeHead(401, {
|
|
69
75
|
"Content-Type": "application/json",
|
|
70
|
-
"WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token"',
|
|
76
|
+
"WWW-Authenticate": 'Bearer realm="boomtick-mcp", error="invalid_token", error_description="Invalid or missing Bearer token"',
|
|
71
77
|
});
|
|
72
78
|
res.end(JSON.stringify({ error: "unauthorized", message: "Invalid or missing Bearer token" }));
|
|
73
79
|
return;
|
package/src/mcp/server.test.ts
CHANGED
|
@@ -100,12 +100,16 @@ describe("BoomtickMCPServer", () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
describe("ListResources", () => {
|
|
103
|
-
it("should return resources list", async () => {
|
|
103
|
+
it("should return resources list containing workspace resources", async () => {
|
|
104
104
|
const handler = requestHandlers.get("resources/list");
|
|
105
105
|
expect(handler).toBeDefined();
|
|
106
106
|
const result = await handler!({ method: "resources/list" });
|
|
107
107
|
expect(result.resources).toBeDefined();
|
|
108
108
|
expect(Array.isArray(result.resources)).toBe(true);
|
|
109
|
+
const uris = result.resources.map((r: any) => r.uri);
|
|
110
|
+
expect(uris).toContain("mcp://workspace/agent-contract");
|
|
111
|
+
expect(uris).toContain("mcp://git/changed-files");
|
|
112
|
+
expect(uris).toContain("mcp://ci/latest-run");
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
it("should return resource templates list", async () => {
|
|
@@ -188,6 +192,35 @@ describe("BoomtickMCPServer", () => {
|
|
|
188
192
|
expect(JSON.parse(result.contents[0].text)).toEqual({ status: "success" });
|
|
189
193
|
});
|
|
190
194
|
|
|
195
|
+
it("should handle mcp://git/changed-files", async () => {
|
|
196
|
+
const handler = requestHandlers.get("resources/read");
|
|
197
|
+
const changedFilesModule = await import("../tools/repo.get_changed_files.js");
|
|
198
|
+
vi.spyOn(changedFilesModule, "getChangedFilesHandler").mockResolvedValue({ files: ["src/index.ts"] } as any);
|
|
199
|
+
|
|
200
|
+
const result = await handler!({
|
|
201
|
+
method: "resources/read",
|
|
202
|
+
params: { uri: "mcp://git/changed-files" },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.contents[0].uri).toBe("mcp://git/changed-files");
|
|
206
|
+
expect(result.contents[0].mimeType).toBe("application/json");
|
|
207
|
+
expect(JSON.parse(result.contents[0].text)).toEqual({ files: ["src/index.ts"] });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should handle mcp://ci/latest-run", async () => {
|
|
211
|
+
const handler = requestHandlers.get("resources/read");
|
|
212
|
+
vi.spyOn(workflowRunsModule, "getWorkflowRunsHandler").mockResolvedValue({ workflowRuns: [{ id: 1, status: "completed", conclusion: "success" }] } as any);
|
|
213
|
+
|
|
214
|
+
const result = await handler!({
|
|
215
|
+
method: "resources/read",
|
|
216
|
+
params: { uri: "mcp://ci/latest-run" },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.contents[0].uri).toBe("mcp://ci/latest-run");
|
|
220
|
+
expect(result.contents[0].mimeType).toBe("application/json");
|
|
221
|
+
expect(JSON.parse(result.contents[0].text)).toEqual({ workflowRuns: [{ id: 1, status: "completed", conclusion: "success" }] });
|
|
222
|
+
});
|
|
223
|
+
|
|
191
224
|
it("should throw when resource is unknown", async () => {
|
|
192
225
|
const handler = requestHandlers.get("resources/read");
|
|
193
226
|
|
package/src/mcp/server.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { getWorkflowRunsHandler, GetWorkflowRunsInputSchema } from "../tools/rep
|
|
|
28
28
|
import { createRepairBranchHandler, CreateRepairBranchInputSchema } from "../tools/repo.create_repair_branch.js";
|
|
29
29
|
import { createBranchHandler, CreateBranchInputSchema } from "../tools/repo.create_branch.js";
|
|
30
30
|
import { runTestsHandler, RunTestsInputSchema } from "../tools/repo.run_tests.js";
|
|
31
|
+
import { checkJulesStatusHandler, CheckJulesStatusInputSchema } from "../tools/repo.check_jules_status.js";
|
|
31
32
|
import { commitPatchHandler, CommitPatchInputSchema } from "../tools/repo.commit_patch.js";
|
|
32
33
|
import { openReplacementPrHandler, OpenReplacementPrInputSchema } from "../tools/github.open_replacement_pr.js";
|
|
33
34
|
import { commentTriageSummaryHandler, CommentTriageSummaryInputSchema } from "../tools/github.comment_triage_summary.js";
|
|
@@ -150,6 +151,16 @@ export class BoomtickMCPServer {
|
|
|
150
151
|
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(changedFiles, null, 2) }],
|
|
151
152
|
};
|
|
152
153
|
}
|
|
154
|
+
if (
|
|
155
|
+
uri === "mcp://ci/latest-run" ||
|
|
156
|
+
uri === "repo://ci/latest-run" ||
|
|
157
|
+
uri === "repo://ci-latest-run"
|
|
158
|
+
) {
|
|
159
|
+
const workflowRuns = await getWorkflowRunsHandler(GetWorkflowRunsInputSchema.parse({}));
|
|
160
|
+
return {
|
|
161
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(workflowRuns, null, 2) }],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
153
164
|
if (uri === "repo://package-json" || uri === "mcp://workspace/package-json") {
|
|
154
165
|
const content = await fs.readFile(path.join(config.repoPath, "package.json"), "utf-8");
|
|
155
166
|
return {
|
|
@@ -234,6 +245,9 @@ export class BoomtickMCPServer {
|
|
|
234
245
|
case "repo.create.repair.branch":
|
|
235
246
|
case "repo.create_repair_branch":
|
|
236
247
|
return createSuccessResult(await createRepairBranchHandler(CreateRepairBranchInputSchema.parse(normArgs)));
|
|
248
|
+
case "repo.check.jules.status":
|
|
249
|
+
case "repo.check_jules_status":
|
|
250
|
+
return createSuccessResult(await checkJulesStatusHandler(CheckJulesStatusInputSchema.parse(normArgs)));
|
|
237
251
|
case "repo.run.tests":
|
|
238
252
|
case "repo.run_tests":
|
|
239
253
|
return createSuccessResult(await runTestsHandler(RunTestsInputSchema.parse(normArgs)));
|
|
@@ -383,24 +397,35 @@ export class BoomtickMCPServer {
|
|
|
383
397
|
}
|
|
384
398
|
}
|
|
385
399
|
|
|
386
|
-
async startHttp(port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10), bearerToken?: string) {
|
|
387
|
-
this.prepareEnvironmentAndPreflight();
|
|
388
|
-
const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
|
|
389
|
-
return await mcpHttp.listen(port);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
400
|
async run() {
|
|
393
401
|
this.prepareEnvironmentAndPreflight();
|
|
394
402
|
|
|
403
|
+
const args = process.argv.slice(2);
|
|
404
|
+
let port = parseInt(process.env.PORT || process.env.HTTP_PORT || "3000", 10);
|
|
405
|
+
let bearerToken = process.env.BOOMTICK_MCP_BEARER_TOKEN || process.env.MCP_BEARER_TOKEN || process.env.AUTH_TOKEN;
|
|
406
|
+
|
|
407
|
+
for (let i = 0; i < args.length; i++) {
|
|
408
|
+
if (args[i] === "--port" && i + 1 < args.length) {
|
|
409
|
+
port = parseInt(args[i + 1], 10);
|
|
410
|
+
i++;
|
|
411
|
+
} else if (args[i].startsWith("--port=")) {
|
|
412
|
+
port = parseInt(args[i].split("=")[1], 10);
|
|
413
|
+
} else if (args[i] === "--bearer-token" && i + 1 < args.length) {
|
|
414
|
+
bearerToken = args[i + 1];
|
|
415
|
+
i++;
|
|
416
|
+
} else if (args[i].startsWith("--bearer-token=")) {
|
|
417
|
+
bearerToken = args[i].split("=")[1];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
395
421
|
const isHttp =
|
|
396
422
|
process.env.MCP_TRANSPORT === "http" ||
|
|
397
|
-
|
|
398
|
-
(process.env.PORT !== undefined && !
|
|
399
|
-
(process.env.HTTP_PORT !== undefined && !
|
|
423
|
+
args.includes("--http") ||
|
|
424
|
+
(process.env.PORT !== undefined && !args.includes("--stdio")) ||
|
|
425
|
+
(process.env.HTTP_PORT !== undefined && !args.includes("--stdio"));
|
|
400
426
|
|
|
401
427
|
if (isHttp) {
|
|
402
|
-
const
|
|
403
|
-
const mcpHttp = createMcpHttpServer({ port, mcpServer: this.server });
|
|
428
|
+
const mcpHttp = createMcpHttpServer({ port, bearerToken, mcpServer: this.server });
|
|
404
429
|
await mcpHttp.listen(port);
|
|
405
430
|
return;
|
|
406
431
|
}
|
package/src/tools/contract.ts
CHANGED
|
@@ -1645,6 +1645,53 @@ export const GetMergeConflictFilesInputJsonSchema = {
|
|
|
1645
1645
|
"type": "object"
|
|
1646
1646
|
} as const;
|
|
1647
1647
|
|
|
1648
|
+
export const GetMergeConflictFilesResponseSchema = z.object({ "hasConflicts": z.boolean(), "conflictedFiles": z.array(z.string()).optional(), "prNumber": z.union([z.number().int(), z.null()]).default(null), "baseBranch": z.string(), "headRef": z.string() });
|
|
1649
|
+
|
|
1650
|
+
export type GetMergeConflictFilesResponse = z.infer<typeof GetMergeConflictFilesResponseSchema>;
|
|
1651
|
+
|
|
1652
|
+
export const GetMergeConflictFilesResponseJsonSchema = {
|
|
1653
|
+
"properties": {
|
|
1654
|
+
"hasConflicts": {
|
|
1655
|
+
"title": "Hasconflicts",
|
|
1656
|
+
"type": "boolean"
|
|
1657
|
+
},
|
|
1658
|
+
"conflictedFiles": {
|
|
1659
|
+
"items": {
|
|
1660
|
+
"type": "string"
|
|
1661
|
+
},
|
|
1662
|
+
"title": "Conflictedfiles",
|
|
1663
|
+
"type": "array"
|
|
1664
|
+
},
|
|
1665
|
+
"prNumber": {
|
|
1666
|
+
"anyOf": [
|
|
1667
|
+
{
|
|
1668
|
+
"type": "integer"
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"type": "null"
|
|
1672
|
+
}
|
|
1673
|
+
],
|
|
1674
|
+
"default": null,
|
|
1675
|
+
"title": "Prnumber"
|
|
1676
|
+
},
|
|
1677
|
+
"baseBranch": {
|
|
1678
|
+
"title": "Basebranch",
|
|
1679
|
+
"type": "string"
|
|
1680
|
+
},
|
|
1681
|
+
"headRef": {
|
|
1682
|
+
"title": "Headref",
|
|
1683
|
+
"type": "string"
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
"required": [
|
|
1687
|
+
"hasConflicts",
|
|
1688
|
+
"baseBranch",
|
|
1689
|
+
"headRef"
|
|
1690
|
+
],
|
|
1691
|
+
"title": "GetMergeConflictFilesResponse",
|
|
1692
|
+
"type": "object"
|
|
1693
|
+
} as const;
|
|
1694
|
+
|
|
1648
1695
|
export const GetPackageScriptsInputSchema = z.object({ "filter": z.union([z.string(), z.null()]).describe("Optional glob pattern to filter script names.").default(null) });
|
|
1649
1696
|
|
|
1650
1697
|
export type GetPackageScriptsInput = z.infer<typeof GetPackageScriptsInputSchema>;
|
|
@@ -3291,6 +3338,44 @@ export const RunTestsInputJsonSchema = {
|
|
|
3291
3338
|
"type": "object"
|
|
3292
3339
|
} as const;
|
|
3293
3340
|
|
|
3341
|
+
export const RunTestsResponseSchema = z.object({ "passed": z.boolean(), "exitCode": z.number().int(), "failedTests": z.array(z.string()).optional(), "results": z.array(z.record(z.string(), z.any())).optional() });
|
|
3342
|
+
|
|
3343
|
+
export type RunTestsResponse = z.infer<typeof RunTestsResponseSchema>;
|
|
3344
|
+
|
|
3345
|
+
export const RunTestsResponseJsonSchema = {
|
|
3346
|
+
"properties": {
|
|
3347
|
+
"passed": {
|
|
3348
|
+
"title": "Passed",
|
|
3349
|
+
"type": "boolean"
|
|
3350
|
+
},
|
|
3351
|
+
"exitCode": {
|
|
3352
|
+
"title": "Exitcode",
|
|
3353
|
+
"type": "integer"
|
|
3354
|
+
},
|
|
3355
|
+
"failedTests": {
|
|
3356
|
+
"items": {
|
|
3357
|
+
"type": "string"
|
|
3358
|
+
},
|
|
3359
|
+
"title": "Failedtests",
|
|
3360
|
+
"type": "array"
|
|
3361
|
+
},
|
|
3362
|
+
"results": {
|
|
3363
|
+
"items": {
|
|
3364
|
+
"additionalProperties": true,
|
|
3365
|
+
"type": "object"
|
|
3366
|
+
},
|
|
3367
|
+
"title": "Results",
|
|
3368
|
+
"type": "array"
|
|
3369
|
+
}
|
|
3370
|
+
},
|
|
3371
|
+
"required": [
|
|
3372
|
+
"passed",
|
|
3373
|
+
"exitCode"
|
|
3374
|
+
],
|
|
3375
|
+
"title": "RunTestsResponse",
|
|
3376
|
+
"type": "object"
|
|
3377
|
+
} as const;
|
|
3378
|
+
|
|
3294
3379
|
export const SearchDdgsInputSchema = z.object({ "query": z.string().describe("The search query."), "maxResults": z.union([z.number().int(), z.null()]).describe("Maximum number of results to return.").default(null) });
|
|
3295
3380
|
|
|
3296
3381
|
export type SearchDdgsInput = z.infer<typeof SearchDdgsInputSchema>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { runTDCli } from "./jules/cli.js";
|
|
3
3
|
|
|
4
4
|
export const CommentTriageSummaryInputSchema = z.object({
|
|
5
5
|
prNumber: z.number(),
|
|
@@ -7,21 +7,12 @@ export const CommentTriageSummaryInputSchema = z.object({
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
export async function commentTriageSummaryHandler(args: z.infer<typeof CommentTriageSummaryInputSchema>) {
|
|
10
|
-
const
|
|
10
|
+
const output = await runTDCli([
|
|
11
11
|
"gh",
|
|
12
12
|
"post-comment",
|
|
13
13
|
"--pr", args.prNumber.toString(),
|
|
14
14
|
"--body", args.body
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
if (result.exitCode !== 0) {
|
|
18
|
-
throw new Error(`Failed to comment on PR: ${result.stderr}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const output = JSON.parse(result.stdout);
|
|
22
|
-
if (output.status === "error") {
|
|
23
|
-
throw new Error(`Failed to comment on PR: ${output.message}`);
|
|
24
|
-
}
|
|
15
|
+
], "comment on PR");
|
|
25
16
|
|
|
26
17
|
return {
|
|
27
18
|
success: true,
|
|
@@ -12,10 +12,13 @@ export async function getMergeConflictFilesHandler(args: z.infer<typeof GetMerge
|
|
|
12
12
|
const headRef = args.headRef || "HEAD";
|
|
13
13
|
const conflictFiles = await getMergeConflictFiles(baseBranch, headRef);
|
|
14
14
|
|
|
15
|
+
const hasConflicts = conflictFiles.length > 0;
|
|
16
|
+
|
|
15
17
|
return {
|
|
16
|
-
|
|
18
|
+
hasConflicts,
|
|
19
|
+
conflictedFiles: conflictFiles,
|
|
20
|
+
prNumber: args.prNumber ?? null,
|
|
17
21
|
baseBranch,
|
|
18
22
|
headRef,
|
|
19
|
-
conflictFiles,
|
|
20
23
|
};
|
|
21
24
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { runCommand } from "../lib/shell.js";
|
|
3
2
|
import { resolveRepo } from "../lib/git.js";
|
|
3
|
+
import { runTDCli } from "./jules/cli.js";
|
|
4
4
|
|
|
5
5
|
export const GetPrDiffInputSchema = z.object({
|
|
6
6
|
prNumber: z.number().describe("The number of the pull request to get the diff for."),
|
|
@@ -11,20 +11,10 @@ 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
|
|
14
|
+
const output = await runTDCli([
|
|
15
15
|
"gh", "pr-diff", params.prNumber.toString(), "--repo", targetRepo
|
|
16
|
-
]);
|
|
16
|
+
], "get PR diff");
|
|
17
17
|
|
|
18
|
-
if (result.exitCode !== 0) {
|
|
19
|
-
throw new Error(`Failed to get PR diff: ${result.stderr}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const output = JSON.parse(result.stdout);
|
|
23
|
-
if (output.status === "error") {
|
|
24
|
-
throw new Error(`Failed to get PR diff: ${output.message}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Every TypeScript tool file must be a pure routing shim
|
|
28
18
|
return {
|
|
29
19
|
prNumber: output.prNumber,
|
|
30
20
|
files: output.files,
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { JulesSession } from "../types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { runTDCli } from "./cli.js";
|
|
4
4
|
|
|
5
5
|
export const CancelJulesSessionInputSchema = z.object({
|
|
6
6
|
sessionId: z.string(),
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
export async function cancelJulesSessionHandler(input: z.infer<typeof CancelJulesSessionInputSchema>): Promise<JulesSession> {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (result.exitCode !== 0) {
|
|
13
|
-
throw new Error(`Failed to cancel session: ${result.stderr}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const output = JSON.parse(result.stdout);
|
|
17
|
-
if (output.status === "error") {
|
|
18
|
-
throw new Error(`Failed to cancel session: ${output.message}`);
|
|
19
|
-
}
|
|
10
|
+
await runTDCli(["agent", "cancel", input.sessionId], "cancel session");
|
|
20
11
|
|
|
21
12
|
return {
|
|
22
13
|
id: input.sessionId.replace("sessions/", ""),
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { runCommand } from "../../lib/shell.js";
|
|
2
|
+
|
|
3
|
+
export interface TDCliResponse {
|
|
4
|
+
status?: string;
|
|
5
|
+
message?: string;
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function tryParseJson(text: string): TDCliResponse | null {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(text) as TDCliResponse;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runTDCli(args: string[], actionName?: string): Promise<TDCliResponse> {
|
|
18
|
+
const result = await runCommand("td", args);
|
|
19
|
+
const stdout = result.stdout.trim();
|
|
20
|
+
const output = stdout.startsWith("{") ? tryParseJson(stdout) : null;
|
|
21
|
+
|
|
22
|
+
if (output?.status === "error") {
|
|
23
|
+
const errorMsg = output.message ?? "Unknown error";
|
|
24
|
+
if (actionName) {
|
|
25
|
+
throw new Error(`Failed to ${actionName}: ${errorMsg}`);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`td returned error: ${errorMsg}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (result.exitCode !== 0) {
|
|
31
|
+
if (actionName) {
|
|
32
|
+
throw new Error(`Failed to ${actionName}: ${result.stderr || stdout}`);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`td command failed (${args.join(" ")}): ${result.stderr || stdout}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!output) {
|
|
38
|
+
throw new Error(`td returned non-JSON output with exit code 0: ${stdout}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return output;
|
|
42
|
+
}
|