Skip to content
Draft
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
8 changes: 8 additions & 0 deletions boomtick-mcp/src/lib/error_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Sanitizes stderr from CLI commands to prevent implementation detail leakage
* in error messages returned to the AI agent.
*/
export function sanitizeError(stderr: string): string {
// Take first line and truncate to 200 chars to balance detail vs security
return (stderr.split("\n")[0] || "Unknown error").slice(0, 200);
}
44 changes: 44 additions & 0 deletions boomtick-mcp/src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import { runPlaywrightHandler, RunPlaywrightInputSchema } from "../tools/repo.ru
import { commitPatchHandler, CommitPatchInputSchema } from "../tools/repo.commit_patch.js";
import { openReplacementPrHandler, OpenReplacementPrInputSchema } from "../tools/github.open_replacement_pr.js";
import { commentTriageSummaryHandler, CommentTriageSummaryInputSchema } from "../tools/github.comment_triage_summary.js";
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 { createJulesSessionHandler, CreateJulesSessionInputSchema } from "../tools/jules/create-session.js";
Expand Down Expand Up @@ -439,6 +442,41 @@ export class BoomtickMCPServer {
required: ["prNumber", "body"],
},
},
{
name: "github.issue_view",
description: "View details of a GitHub issue including title, body, and state.",
inputSchema: {
type: "object",
properties: {
issueNumber: { type: "number", description: "The number of the issue to view." },
},
required: ["issueNumber"],
},
},
{
name: "github.issue_update",
description: "Update the body of a GitHub issue.",
inputSchema: {
type: "object",
properties: {
issueNumber: { type: "number", description: "The number of the issue to update." },
body: { type: "string", description: "The new body content for the issue." },
},
required: ["issueNumber", "body"],
},
},
{
name: "github.issue_comment",
description: "Add a new comment to a GitHub issue.",
inputSchema: {
type: "object",
properties: {
issueNumber: { type: "number", description: "The number of the issue to comment on." },
body: { type: "string", description: "The content of the comment." },
},
required: ["issueNumber", "body"],
},
},
{
name: "jules.create_session",
description: "Create a Jules session that performs work externally and may generate a GitHub pull request.",
Expand Down Expand Up @@ -584,6 +622,12 @@ export class BoomtickMCPServer {
return createSuccessResult(await openReplacementPrHandler(OpenReplacementPrInputSchema.parse(request.params.arguments)));
case "github.comment_triage_summary":
return createSuccessResult(await commentTriageSummaryHandler(CommentTriageSummaryInputSchema.parse(request.params.arguments)));
case "github.issue_view":
return createSuccessResult(await issueViewHandler(IssueViewInputSchema.parse(request.params.arguments)));
case "github.issue_update":
return createSuccessResult(await issueUpdateHandler(IssueUpdateInputSchema.parse(request.params.arguments)));
case "github.issue_comment":
return createSuccessResult(await issueCommentHandler(IssueCommentInputSchema.parse(request.params.arguments)));



Expand Down
31 changes: 31 additions & 0 deletions boomtick-mcp/src/tools/github.issue_comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from "zod";
import { runCommand } from "../lib/shell.js";
import { sanitizeError } from "../lib/error_utils.js";

export const IssueCommentInputSchema = z.object({
issueNumber: z.number().describe("The number of the issue to comment on."),
body: z.string().min(1, "Comment body cannot be empty").describe("The content of the comment."),
});

const IssueCommentOutputSchema = z.object({
status: z.string(),
comment: z.any().optional(),
message: z.string().optional(),
});

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

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

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

const output = IssueCommentOutputSchema.parse(JSON.parse(result.stdout));
if (output.status === "error") {
throw new Error(`Failed to post comment: ${output.message}`);
}

return { status: "success", comment: output.comment };
}
31 changes: 31 additions & 0 deletions boomtick-mcp/src/tools/github.issue_update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from "zod";
import { runCommand } from "../lib/shell.js";
import { sanitizeError } from "../lib/error_utils.js";

export const IssueUpdateInputSchema = z.object({
issueNumber: z.number().describe("The number of the issue to update."),
body: z.string().min(1, "Issue body cannot be empty").describe("The new body content for the issue."),
});

const IssueUpdateOutputSchema = z.object({
status: z.string(),
issue: z.any().optional(),
message: z.string().optional(),
});

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

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

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

const output = IssueUpdateOutputSchema.parse(JSON.parse(result.stdout));
if (output.status === "error") {
throw new Error(`Failed to update issue: ${output.message}`);
}

return { status: "success", issue: output.issue };
}
35 changes: 35 additions & 0 deletions boomtick-mcp/src/tools/github.issue_view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { z } from "zod";
import { runCommand } from "../lib/shell.js";
import { sanitizeError } from "../lib/error_utils.js";

export const IssueViewInputSchema = z.object({
issueNumber: z.number().describe("The number of the issue to view."),
});

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

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

const result = await runCommand("td-cli", ["gh", "issue-view", params.issueNumber.toString()]);

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

const output = IssueViewOutputSchema.parse(JSON.parse(result.stdout));
if (output.status === "error") {
throw new Error(`Failed to view issue: ${output.message}`);
}

return { issue: output.issue };
}
55 changes: 51 additions & 4 deletions dev-tools/cli-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@
}
]
},
"gh post-comment": {
"description": "Adds a new comment to a Pull Request from a file or literal text.",
"exact_usage": "python3 dev-tools/td_cli.py gh post-comment --pr <PR_NUMBER> [--file <PATH> | --body <TEXT>]",
"required_flags": [
{
"flag": "--pr",
"type": "integer",
"description": "The GitHub Pull Request ID."
}
],
"optional_flags": [
{ "flag": "--file", "type": "string", "description": "Path to the Markdown file containing the comment body." },
{ "flag": "--body", "type": "string", "description": "The literal body text for the comment." }
]
},
"gh detect-conflicts": {
"description": "Detects conflicts for a single, specific Pull Request.",
"exact_usage": "python3 dev-tools/td_cli.py gh detect-conflicts --pr <PR_NUMBER>",
Expand Down Expand Up @@ -194,11 +209,43 @@
"required_flags": []
},
"gh create-issue": {
"description": "Creates a new GitHub issue using a title and a markdown file for the body.",
"exact_usage": "python3 dev-tools/td_cli.py gh create-issue --title <TITLE> --file <PATH_TO_BODY_FILE>",
"description": "Creates a new GitHub issue using a title and either a markdown file or literal text for the body.",
"exact_usage": "python3 dev-tools/td_cli.py gh create-issue --title <TITLE> [--file <PATH> | --body <TEXT>]",
"required_flags": [
{ "flag": "--title", "type": "string", "description": "The title of the issue." },
{ "flag": "--file", "type": "string", "description": "Path to the Markdown file containing the issue body." }
{ "flag": "--title", "type": "string", "description": "The title of the issue." }
],
"optional_flags": [
{ "flag": "--file", "type": "string", "description": "Path to the Markdown file containing the issue body." },
{ "flag": "--body", "type": "string", "description": "The literal body text for the issue." }
]
},
"gh issue-view": {
"description": "Fetches details of a GitHub issue including title, body, and state.",
"exact_usage": "python3 dev-tools/td_cli.py gh issue-view <ISSUE_NUMBER>",
"required_arguments": [
{ "name": "issue_number", "type": "integer", "description": "The GitHub issue number." }
]
},
"gh issue-update": {
"description": "Updates the body of a GitHub issue from a file or literal text.",
"exact_usage": "python3 dev-tools/td_cli.py gh issue-update <ISSUE_NUMBER> [--file <PATH> | --body <TEXT>]",
"required_arguments": [
{ "name": "issue_number", "type": "integer", "description": "The GitHub issue number." }
],
"optional_flags": [
{ "flag": "--file", "type": "string", "description": "Path to the Markdown file containing the new issue body." },
{ "flag": "--body", "type": "string", "description": "The literal body text for the issue." }
]
},
"gh issue-comment": {
"description": "Adds a new comment to a GitHub issue from a file or literal text.",
"exact_usage": "python3 dev-tools/td_cli.py gh issue-comment <ISSUE_NUMBER> [--file <PATH> | --body <TEXT>]",
"required_arguments": [
{ "name": "issue_number", "type": "integer", "description": "The GitHub issue number." }
],
"optional_flags": [
{ "flag": "--file", "type": "string", "description": "Path to the Markdown file containing the comment body." },
{ "flag": "--body", "type": "string", "description": "The literal body text for the comment." }
]
},
"gh validate-issue": {
Expand Down
86 changes: 75 additions & 11 deletions dev-tools/tdw_services/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ def err(ctx, msg, code=1, data=None):
click.echo(f"❌ Error: {msg}", err=True)
sys.exit(code)

def _handle_unexpected_error(ctx, command_name, e):
from tdw_services.utils import log_error
log_error(f"Unexpected error in {command_name}: {e}")
err(ctx, f"An unexpected error occurred in {command_name}.")

def _get_body_content(ctx, orch, file, body):
content = body if body is not None else (orch._read_safe_file(file) if file else None)
if content is None:
err(ctx, "Provide --file or --body")
return content

# ==========================================
# REPO COMMAND GROUP
# ==========================================
Expand Down Expand Up @@ -170,18 +181,69 @@ def audit_pr(ctx, pr_number, fetch, run_audit, submit, cleanup, dry_run, base, e

@gh.command()
@click.option('--title', required=True, help='Issue title')
@click.option('--file', required=True, help='Path to file containing issue body')
@click.option('--file', help='Path to file containing issue body')
@click.option('--body', help='Literal body text')
@click.pass_context
def create_issue(ctx, title, file, body):
"""Create a new GitHub issue."""
orch = ctx.obj['ORCHESTRATOR']
try:
content = _get_body_content(ctx, orch, file, body)
res = orch.create_issue(title, content)
out(ctx, f"✅ Successfully created issue: {res.get('html_url')}", data={"issue": res})
except CLIError as e:
err(ctx, str(e), code=e.code)
except Exception as e:
_handle_unexpected_error(ctx, "create-issue", e)

@gh.command()
@click.argument('issue_number', type=int)
@click.pass_context
def issue_view(ctx, issue_number):
"""View details of a GitHub issue."""
orch = ctx.obj['ORCHESTRATOR']
try:
issue = orch.get_issue_details(issue_number)
msg = f"Issue #{issue.get('number')}: {issue.get('title')}\nState: {issue.get('state')}\n\n{issue.get('body')}"
out(ctx, msg, data={"issue": issue})
except CLIError as e:
err(ctx, str(e), code=e.code)
except Exception as e:
_handle_unexpected_error(ctx, "issue-view", e)

@gh.command()
@click.argument('issue_number', type=int)
@click.option('--file', help='Path to file containing new issue body')
@click.option('--body', help='Literal body text')
@click.pass_context
def issue_update(ctx, issue_number, file, body):
"""Update a GitHub issue's body."""
orch = ctx.obj['ORCHESTRATOR']
try:
content = _get_body_content(ctx, orch, file, body)
res = orch.update_issue_body(issue_number, content)
out(ctx, f"✅ Successfully updated issue #{issue_number}", data={"issue": res})
except CLIError as e:
err(ctx, str(e), code=e.code)
except Exception as e:
_handle_unexpected_error(ctx, "issue-update", e)

@gh.command()
@click.argument('issue_number', type=int)
@click.option('--file', help='Path to file containing comment body')
@click.option('--body', help='Literal body text')
@click.pass_context
def create_issue(ctx, title, file):
"""Create a new GitHub issue from a file."""
def issue_comment(ctx, issue_number, file, body):
"""Post a comment to a GitHub issue."""
orch = ctx.obj['ORCHESTRATOR']
try:
res = orch.create_issue(title, file)
out(ctx, f"✅ Successfully created issue: {res.get('html_url')}", data=res)
content = _get_body_content(ctx, orch, file, body)
res = orch.post_comment(issue_number, content)
out(ctx, f"✅ Successfully posted comment to issue #{issue_number}", data={"comment": res})
except CLIError as e:
err(ctx, str(e), code=e.code)
except Exception as e:
err(ctx, str(e))
_handle_unexpected_error(ctx, "issue-comment", e)

@gh.command()
@click.option('--issue-number', type=int)
Expand Down Expand Up @@ -260,18 +322,20 @@ def detect_conflicts(ctx, pr):

@gh.command()
@click.option('--pr', required=True, type=int, help="The PR number to comment on.")
@click.option('--file', required=True, type=str, help="Path to the file containing the comment body.")
@click.option('--file', type=str, help="Path to the file containing the comment body.")
@click.option('--body', type=str, help="Literal comment text.")
@click.pass_context
def post_comment(ctx, pr, file):
"""Post a comment to a PR from a file."""
def post_comment(ctx, pr, file, body):
"""Post a comment to a PR."""
orch = ctx.obj['ORCHESTRATOR']
try:
res = orch.post_comment(pr, file)
content = _get_body_content(ctx, orch, file, body)
res = orch.post_comment(pr, content)
out(ctx, f"✅ Successfully posted comment to PR #{pr}", data=res)
except CLIError as e:
err(ctx, str(e), code=e.code)
except Exception as e:
err(ctx, str(e))
_handle_unexpected_error(ctx, "post-comment", e)

@gh.command()
@click.pass_context
Expand Down
Loading