Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions boomtick-pkg/cli/dev_tools/td_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
if "pytest" not in sys.modules:
sys.exit(1)

# Add the dev-tools directory to sys.path so we can import tdw_services
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# Add the CLI package root and dev-tools directory to sys.path so we can import tdw_services and its dependencies
current_dir = os.path.dirname(os.path.abspath(__file__))
package_root = os.path.dirname(current_dir)
if package_root not in sys.path:
sys.path.append(package_root)
if current_dir not in sys.path:
sys.path.append(current_dir)

try:
from tdw_services.cli import cli
Expand Down
12 changes: 12 additions & 0 deletions boomtick-pkg/mcp/src/mcp/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,18 @@ export const MCP_TOOLS: Tool[] = [
required: ["issueNumber", "body"],
},
},
{
name: "github.create_issue",
description: "Create a new GitHub issue.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "The title of the issue." },
body: { type: "string", description: "The body/description of the issue." },
},
required: ["title", "body"],
},
},
{
name: "jules.create_session",
description: "Create a Jules session that performs work externally and may generate a GitHub pull request.",
Expand Down
3 changes: 3 additions & 0 deletions boomtick-pkg/mcp/src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { createPullRequestHandler, CreatePullRequestInputSchema } from "../tools
import { issueViewHandler, IssueViewInputSchema } from "../tools/github.issue_view.js";
import { issueUpdateHandler, IssueUpdateInputSchema } from "../tools/github.issue_update.js";
import { issueCommentHandler, IssueCommentInputSchema } from "../tools/github.issue_comment.js";
import { createIssueHandler, CreateIssueInputSchema } from "../tools/github.create_issue.js";


import { createJulesSessionHandler, CreateJulesSessionInputSchema } from "../tools/jules/create-session.js";
Expand Down Expand Up @@ -221,6 +222,8 @@ export class BoomtickMCPServer {
return createSuccessResult(await issueUpdateHandler(IssueUpdateInputSchema.parse(request.params.arguments)));
case "github.issue_comment":
return createSuccessResult(await issueCommentHandler(IssueCommentInputSchema.parse(request.params.arguments)));
case "github.create_issue":
return createSuccessResult(await createIssueHandler(CreateIssueInputSchema.parse(request.params.arguments)));



Expand Down
63 changes: 63 additions & 0 deletions boomtick-pkg/mcp/src/tools/github.create_issue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, vi } from "vitest";
import { createIssueHandler } from "./github.create_issue.js";
import * as shell from "../lib/shell.js";

vi.mock("../lib/shell.js", () => ({
runCommand: vi.fn()
}));

describe("github.create_issue", () => {
it("should create an issue successfully", async () => {
const mockResponse = {
status: "success",
issue: {
number: 123,
title: "Test Issue",
html_url: "https://github.com/owner/repo/issues/123",
state: "open"
}
};

vi.mocked(shell.runCommand).mockResolvedValue({
stdout: JSON.stringify(mockResponse),
stderr: "",
exitCode: 0,
durationMs: 10,
command: "td-cli gh create-issue --title 'Test Issue' --body 'Test Body'"
});

const result = await createIssueHandler({ title: "Test Issue", body: "Test Body" });
expect(result.status).toBe("success");
expect(result.issue?.number).toBe(123);
expect(result.issue?.title).toBe("Test Issue");
});

it("should throw error on command failure", async () => {
vi.mocked(shell.runCommand).mockResolvedValue({
stdout: "",
stderr: "Auth failed",
exitCode: 1,
durationMs: 10,
command: "td-cli gh create-issue"
});

await expect(createIssueHandler({ title: "Test Issue", body: "Test Body" })).rejects.toThrow("Failed to create issue: Auth failed");
});

it("should handle error status from CLI output", async () => {
const mockResponse = {
status: "error",
message: "Repo not found"
};

vi.mocked(shell.runCommand).mockResolvedValue({
stdout: JSON.stringify(mockResponse),
stderr: "",
exitCode: 0,
durationMs: 10,
command: "td-cli gh create-issue"
});

await expect(createIssueHandler({ title: "Test Issue", body: "Test Body" })).rejects.toThrow("Failed to create issue: Repo not found");
});
});
43 changes: 43 additions & 0 deletions boomtick-pkg/mcp/src/tools/github.create_issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { z } from "zod";
import { runCommand } from "../lib/shell.js";
import { sanitizeError } from "../lib/error_utils.js";

export const CreateIssueInputSchema = z.object({
title: z.string().min(1, "Issue title cannot be empty").describe("The title of the issue."),
body: z.string().min(1, "Issue body cannot be empty").describe("The body/description of the issue."),
});

const CreateIssueOutputSchema = z.object({
status: z.string(),
issue: z.object({
number: z.number(),
title: z.string(),
html_url: z.string(),
state: z.string(),
}).optional(),
message: z.string().optional(),
});

export async function createIssueHandler(args: z.infer<typeof CreateIssueInputSchema>) {
const params = CreateIssueInputSchema.parse(args);

const result = await runCommand("td-cli", ["gh", "create-issue", "--title", params.title, "--body", params.body]);

if (result.exitCode !== 0) {
throw new Error(`Failed to create issue: ${sanitizeError(result.stderr)}`);
}

let output;
try {
output = JSON.parse(result.stdout);
} catch (e) {
throw new Error(`Failed to parse CLI output: ${result.stdout}`);
}

const parsedOutput = CreateIssueOutputSchema.parse(output);
if (parsedOutput.status === "error") {
throw new Error(`Failed to create issue: ${parsedOutput.message}`);
}

return { status: "success", issue: parsedOutput.issue };
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"lint:eslint": "eslint . --max-warnings 0",
"lint:types": "tsc --noEmit",
"audit:semgrep": "semgrep scan --config auto --error",
"audit:anti-patterns": "node scripts/detect-antipatterns.mjs",
"audit:anti-patterns": "PYTHONPATH=boomtick-pkg/cli:boomtick-pkg/cli/dev_tools node scripts/detect-antipatterns.mjs",
"audit:dead-code": "pnpm exec knip --exclude exports",
"ci:local": "run-s lint type-check test audit:anti-patterns audit:dead-code audit:semgrep",
"knip": "knip --exclude exports",
Expand Down
Loading