tool call logging, centralized temp dir

This commit is contained in:
David Blass
2025-11-19 18:26:15 -05:00
parent 4b43b617f0
commit 7e0dcd5374
13 changed files with 781 additions and 354 deletions
+4 -21
View File
@@ -2,9 +2,9 @@
* Simple MCP configuration helper for adding our minimal GitHub comment server
*/
import type { McpServerConfig, McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs";
import { log } from "../utils/cli.ts";
import type { Mode } from "../modes.ts";
import { parseRepoContext } from "../utils/github.ts";
import { ghPullfrogMcpName } from "./index.ts";
@@ -12,7 +12,7 @@ export type McpName = typeof ghPullfrogMcpName;
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
export function createMcpConfigs(githubInstallationToken: string, modes: Mode[]): McpConfigs {
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
@@ -27,25 +27,8 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
env: {
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
PULLFROG_MODES: JSON.stringify(modes),
},
},
};
}
/**
* Iterate through MCP servers and call the provided handler for each stdio server
* Shared logic to avoid duplication across agents
*/
export function forEachStdioMcpServer(
mcpServers: Record<string, McpServerConfig>,
handler: (serverName: string, serverConfig: McpStdioServerConfig) => void
): void {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
// Only configure stdio servers (CLIs support stdio MCP servers)
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
handler(serverName, serverConfig);
}
}
+55
View File
@@ -0,0 +1,55 @@
import { type } from "arktype";
import type { Mode } from "../modes.ts";
import { contextualize, tool } from "./shared.ts";
// Get modes from environment variable (set by createMcpConfigs)
function getModes(): Mode[] {
const modesJson = process.env.PULLFROG_MODES;
if (modesJson) {
try {
return JSON.parse(modesJson);
} catch {
return [];
}
}
return [];
}
export const SelectMode = type({
modeName: type.string.describe(
"the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')"
),
});
export const SelectModeTool = tool({
name: "select_mode",
description:
"Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
parameters: SelectMode,
execute: contextualize(async ({ modeName }) => {
const allModes = getModes();
if (allModes.length === 0) {
return {
error:
"No modes available. Modes must be provided via PULLFROG_MODES environment variable.",
};
}
const selectedMode = allModes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (!selectedMode) {
const availableModes = allModes.map((m) => m.name).join(", ");
return {
error: `Mode "${modeName}" not found. Available modes: ${availableModes}`,
availableModes: allModes.map((m) => ({ name: m.name, description: m.description })),
};
}
return {
modeName: selectedMode.name,
description: selectedMode.description,
prompt: selectedMode.prompt,
};
}),
});
+2 -2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env node
// Minimal GitHub Issue Comment MCP Server
import { FastMCP } from "fastmcp";
import {
CreateCommentTool,
@@ -11,6 +9,7 @@ import { IssueTool } from "./issue.ts";
import { PullRequestTool } from "./pr.ts";
import { PullRequestInfoTool } from "./prInfo.ts";
import { ReviewTool } from "./review.ts";
import { SelectModeTool } from "./selectMode.ts";
import { addTools } from "./shared.ts";
const server = new FastMCP({
@@ -19,6 +18,7 @@ const server = new FastMCP({
});
addTools(server, [
SelectModeTool,
CreateCommentTool,
EditCommentTool,
CreateWorkingCommentTool,
+15 -48
View File
@@ -1,9 +1,8 @@
import { appendFileSync } from "node:fs";
import { join } from "node:path";
import { cached } from "@ark/util";
import { Octokit } from "@octokit/rest";
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { FastMCP, Tool } from "fastmcp";
import { log } from "../utils/cli.ts";
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
export interface ToolResult {
@@ -32,49 +31,6 @@ export interface McpContext extends RepoContext {
octokit: Octokit;
}
/**
* Get the log file path
*/
function getLogPath(): string {
return join(process.cwd(), "log.txt");
}
/**
* Log MCP tool call information to log.txt
*/
function logToolCall({
toolName,
request,
error,
success,
}: {
toolName: string;
request: unknown;
error?: unknown;
success?: boolean;
}) {
const logPath = getLogPath();
const timestamp = new Date().toISOString();
const requestStr = JSON.stringify(request, null, 2);
let logEntry = `[${timestamp}] Tool: ${toolName}\nRequest: ${requestStr}\n`;
if (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
logEntry += `Error: ${errorMessage}\n`;
if (errorStack) {
logEntry += `Stack: ${errorStack}\n`;
}
logEntry += `Status: FAILED\n`;
} else if (success !== undefined) {
logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}\n`;
}
logEntry += `${"=".repeat(80)}\n\n`;
appendFileSync(logPath, logEntry, "utf-8");
}
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
// Wrap the execute function to add logging with the tool name
const toolName = toolDef.name;
@@ -82,15 +38,26 @@ export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>)
toolDef.execute = async (args: params, context: any) => {
try {
logToolCall({ toolName, request: args });
const result = await originalExecute(args, context);
// Check if result is a ToolResult with isError property
const isError =
result && typeof result === "object" && "isError" in result && result.isError === true;
logToolCall({ toolName, request: args, success: !isError });
const resultData =
result && typeof result === "object" && "content" in result
? (result as ToolResult).content[0]?.text
: undefined;
if (isError && resultData) {
log.toolCall({ toolName, request: args, error: resultData });
} else if (resultData) {
log.toolCall({ toolName, request: args, result: resultData });
} else {
log.toolCall({ toolName, request: args });
}
return result;
} catch (error) {
logToolCall({ toolName, request: args, error });
const errorMessage = error instanceof Error ? error.message : String(error);
log.toolCall({ toolName, request: args, error: errorMessage });
throw error;
}
};