From 4ff547f67342bd5969b0e762a1a158ac3dcfdb49 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 25 Nov 2025 17:07:40 -0800 Subject: [PATCH] add debug mcp tool for testing, fix transport issues --- agents/codex.ts | 4 ++-- fixtures/basic.txt | 2 +- mcp/debug.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ mcp/server.ts | 2 ++ modes.ts | 2 +- play.ts | 2 ++ utils/shell.ts | 16 ++++++++++++++-- 7 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 mcp/debug.ts diff --git a/agents/codex.ts b/agents/codex.ts index 413e56d..287d4f4 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -128,7 +128,7 @@ const messageHandlers: { toolName: item.tool, input: { server: item.server, - ...((item as any).args || {}), + ...((item as any).arguments || {}), }, }); } @@ -167,7 +167,7 @@ const messageHandlers: { const reasoningText = item.text.trim(); // Remove markdown bold markers if present for cleaner output const cleanText = reasoningText.replace(/\*\*/g, ""); - log.info(cleanText); + log.box(cleanText, { title: "Codex" }); } }, error: (event) => { diff --git a/fixtures/basic.txt b/fixtures/basic.txt index 482eeb8..b0ea008 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -write a comment to https://github.com/pullfrogai/scratch/pull/29 that tells a joke \ No newline at end of file +use debug_shell_command tool to run 'git status' and explain the result \ No newline at end of file diff --git a/mcp/debug.ts b/mcp/debug.ts new file mode 100644 index 0000000..2a58d1e --- /dev/null +++ b/mcp/debug.ts @@ -0,0 +1,45 @@ +import { type } from "arktype"; +import { $ } from "../utils/shell.ts"; +import type { ToolResult } from "./shared.ts"; +import { tool } from "./shared.ts"; + +export const DebugShellCommand = type({}); + +export const DebugShellCommandTool = tool({ + name: "debug_shell_command", + description: + "debug tool: runs 'git status' and returns the output. use this to test shell command execution in the MCP server.", + parameters: DebugShellCommand, + execute: async (): Promise => { + try { + const result = $("git", ["status"]); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + success: true, + command: "git status", + output: result.trim(), + }, + null, + 2 + ), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + }, +}); diff --git a/mcp/server.ts b/mcp/server.ts index cfa8ab8..ddfbee0 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -9,6 +9,7 @@ import { EditCommentTool, UpdateWorkingCommentTool, } from "./comment.ts"; +import { DebugShellCommandTool } from "./debug.ts"; import { IssueTool } from "./issue.ts"; import { PullRequestTool } from "./pr.ts"; import { PullRequestInfoTool } from "./prInfo.ts"; @@ -35,6 +36,7 @@ addTools(server, [ GetReviewCommentsTool, ListPullRequestReviewsTool, GetCheckSuiteLogsTool, + DebugShellCommandTool, ]); server.start(); diff --git a/modes.ts b/modes.ts index 3b3e2cd..d2bb1b7 100644 --- a/modes.ts +++ b/modes.ts @@ -6,7 +6,7 @@ export interface Mode { prompt: string; } -const initialCommentInstruction = `Use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment with a conversational description of what work you are about to perform.`; +const initialCommentInstruction = `When the task is associated with an issue/PR, use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment with a conversational description of what work you are about to perform.`; export const modes: Mode[] = [ { diff --git a/play.ts b/play.ts index 5dd600f..75e858a 100644 --- a/play.ts +++ b/play.ts @@ -141,6 +141,8 @@ Examples: if (!result.success) { process.exit(1); } + + process.exit(0); } catch (err) { log.error((err as Error).message); process.exit(1); diff --git a/utils/shell.ts b/utils/shell.ts index fe85b11..2490215 100644 --- a/utils/shell.ts +++ b/utils/shell.ts @@ -29,8 +29,11 @@ interface ShellOptions { */ export function $(cmd: string, args: string[], options?: ShellOptions): string { const encoding = options?.encoding ?? "utf-8"; + + // CRITICAL: use "ignore" for stdin instead of "inherit" to avoid breaking MCP transport + // when running inside an MCP server, stdin is used for JSON-RPC protocol const result = spawnSync(cmd, args, { - stdio: ["inherit", "pipe", "pipe"], + stdio: ["ignore", "pipe", "pipe"], encoding, cwd: options?.cwd, }); @@ -39,10 +42,19 @@ export function $(cmd: string, args: string[], options?: ShellOptions): string { const stderr = result.stderr ?? ""; // Write output to process streams so it behaves like stdio: "inherit" + // CRITICAL: when running inside an MCP server, stdout is used for JSON-RPC protocol + // so we must write to stderr instead to avoid corrupting the protocol // Only log if log option is not explicitly set to false if (options?.log !== false) { + // if stdout is a TTY, it's safe to write to it; otherwise it's likely a pipe used for JSON-RPC + const canWriteToStdout = process.stdout.isTTY === true; if (stdout) { - process.stdout.write(stdout); + if (canWriteToStdout) { + process.stdout.write(stdout); + } else { + // stdout is a pipe (MCP context) - write to stderr instead + process.stderr.write(stdout); + } } if (stderr) { process.stderr.write(stderr);