From 579c79e38c7b3818e25108c580a6808fa8d90090 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Wed, 19 Nov 2025 17:30:28 -0500 Subject: [PATCH 1/6] begin gemini depencency download removal --- agents/gemini.ts | 12 ++-- agents/shared.ts | 145 +++++++++++++++++++++++++++++++++++++++++++++ fixtures/basic.txt | 2 +- play.ts | 2 +- 4 files changed, 153 insertions(+), 8 deletions(-) diff --git a/agents/gemini.ts b/agents/gemini.ts index 05ae52f..01fbfbe 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -2,17 +2,17 @@ import { spawnSync } from "node:child_process"; import { log } from "../utils/cli.ts"; import { spawn } from "../utils/subprocess.ts"; import { addInstructions } from "./instructions.ts"; -import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts"; +import { agent, type ConfigureMcpServersParams, installFromGithub } from "./shared.ts"; export const gemini = agent({ name: "gemini", inputKeys: ["google_api_key", "gemini_api_key"], install: async () => { - return await installFromNpmTarball({ - packageName: "@google/gemini-cli", - version: "latest", - executablePath: "dist/index.js", - installDependencies: true, + return await installFromGithub({ + owner: "google-gemini", + repo: "gemini-cli", + tag: "v0.16.0", + assetName: "gemini.js", }); }, run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => { diff --git a/agents/shared.ts b/agents/shared.ts index 5a36399..8e4285e 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -55,6 +55,17 @@ export interface InstallFromCurlParams { executableName: string; } +/** + * Parameters for installing from GitHub releases + */ +export interface InstallFromGithubParams { + owner: string; + repo: string; + tag?: string; + assetName?: string; + executablePath?: string; +} + /** * NPM registry response data structure */ @@ -172,6 +183,140 @@ export async function installFromNpmTarball({ return cliPath; } +/** + * Install a CLI tool from GitHub releases + * Downloads the latest release asset from GitHub and returns the path to the executable + * The temp directory will be cleaned up by the OS automatically + */ +export async function installFromGithub({ + owner, + repo, + tag, + assetName, + executablePath, +}: InstallFromGithubParams): Promise { + log.info(`📦 Installing ${owner}/${repo} from GitHub releases...`); + + // fetch release from GitHub API (specific tag or latest) + const releaseUrl = tag + ? `https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}` + : `https://api.github.com/repos/${owner}/${repo}/releases/latest`; + log.info(`Fetching release from ${releaseUrl}...`); + const releaseResponse = await fetch(releaseUrl); + if (!releaseResponse.ok) { + throw new Error( + `Failed to fetch release: ${releaseResponse.status} ${releaseResponse.statusText}` + ); + } + + const releaseData = (await releaseResponse.json()) as { + tag_name: string; + assets: Array<{ + name: string; + browser_download_url: string; + }>; + }; + + log.info(`Found release: ${releaseData.tag_name}`); + + // find the asset to download + let assetUrl: string; + if (assetName) { + const asset = releaseData.assets.find((a) => a.name === assetName); + if (!asset) { + throw new Error(`Asset '${assetName}' not found in release ${releaseData.tag_name}`); + } + assetUrl = asset.browser_download_url; + } else { + // if no asset name specified, try to find a .js file or use the first asset + const jsAsset = releaseData.assets.find((a) => a.name.endsWith(".js")); + if (!jsAsset) { + if (releaseData.assets.length === 0) { + throw new Error(`No assets found in release ${releaseData.tag_name}`); + } + assetUrl = releaseData.assets[0].browser_download_url; + } else { + assetUrl = jsAsset.browser_download_url; + } + } + + log.info(`Downloading asset from ${assetUrl}...`); + + // create temp directory + const tempDirPrefix = `${owner}-${repo}-github-`; + const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix)); + + // determine file extension and download path + const urlPath = new URL(assetUrl).pathname; + const fileName = urlPath.split("/").pop() || "asset"; + const downloadPath = join(tempDir, fileName); + + // download the asset + const assetResponse = await fetch(assetUrl); + if (!assetResponse.ok) { + throw new Error( + `Failed to download asset: ${assetResponse.status} ${assetResponse.statusText}` + ); + } + + if (!assetResponse.body) throw new Error("Response body is null"); + const fileStream = createWriteStream(downloadPath); + await pipeline(assetResponse.body, fileStream); + log.info(`Downloaded asset to ${downloadPath}`); + + // determine the executable path + let cliPath: string; + if (executablePath) { + // if executablePath is provided, assume the downloaded file needs to be extracted + // and the executable is inside + if (fileName.endsWith(".tar.gz") || fileName.endsWith(".tgz")) { + // extract tarball + log.info(`Extracting tarball...`); + const extractResult = spawnSync("tar", ["-xzf", downloadPath, "-C", tempDir], { + stdio: "pipe", + encoding: "utf-8", + }); + if (extractResult.status !== 0) { + throw new Error( + `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}` + ); + } + // find the extracted directory (usually named after the repo) + const extractedDir = join(tempDir, repo); + cliPath = join(extractedDir, executablePath); + } else if (fileName.endsWith(".zip")) { + // extract zip + log.info(`Extracting zip...`); + const extractResult = spawnSync("unzip", ["-q", downloadPath, "-d", tempDir], { + stdio: "pipe", + encoding: "utf-8", + }); + if (extractResult.status !== 0) { + throw new Error( + `Failed to extract zip: ${extractResult.stderr || extractResult.stdout || "Unknown error"}` + ); + } + const extractedDir = join(tempDir, repo); + cliPath = join(extractedDir, executablePath); + } else { + // assume it's a single file, executablePath is relative to temp dir + cliPath = join(tempDir, executablePath); + } + } else { + // no executablePath, assume the downloaded file is the executable + cliPath = downloadPath; + } + + if (!existsSync(cliPath)) { + throw new Error(`Executable not found at ${cliPath}`); + } + + chmodSync(cliPath, 0o755); + log.info(`✓ Installed from GitHub release at ${cliPath}`); + + return cliPath; +} + /** * Install a CLI tool from a curl-based install script * Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable diff --git a/fixtures/basic.txt b/fixtures/basic.txt index 789260c..48607c8 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -create a comment on https://github.com/pullfrogai/scratch/pull/29 that says ribbit \ No newline at end of file +create a comment on https://github.com/pullfrogai/scratch/pull/29 that says Things just got out of hand \ No newline at end of file diff --git a/play.ts b/play.ts index 276111c..5e972a6 100644 --- a/play.ts +++ b/play.ts @@ -24,7 +24,7 @@ export async function run( const inputs: Required = { prompt, - agent: "claude", + agent: "gemini", ...flatMorph(agents, (_, agent) => agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) ), From 295949c173c52ff830f14e419571ba1d26d8b3e0 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Thu, 20 Nov 2025 06:53:57 -0500 Subject: [PATCH 2/6] use github release for gemini --- agents/gemini.ts | 2 +- agents/shared.ts | 59 ++++-------------------------------------------- 2 files changed, 6 insertions(+), 55 deletions(-) diff --git a/agents/gemini.ts b/agents/gemini.ts index 01fbfbe..84a826b 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -32,7 +32,7 @@ export const gemini = agent({ try { const result = await spawn({ cmd: "node", - args: [cliPath, "--yolo", "--output-format", "text", sessionPrompt], + args: [cliPath, "--yolo", "--output-format", "text", "-p", sessionPrompt], env: { GEMINI_API_KEY: apiKey, GITHUB_INSTALLATION_TOKEN: githubInstallationToken, diff --git a/agents/shared.ts b/agents/shared.ts index 8e4285e..47127a5 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -219,26 +219,11 @@ export async function installFromGithub({ log.info(`Found release: ${releaseData.tag_name}`); - // find the asset to download - let assetUrl: string; - if (assetName) { - const asset = releaseData.assets.find((a) => a.name === assetName); - if (!asset) { - throw new Error(`Asset '${assetName}' not found in release ${releaseData.tag_name}`); - } - assetUrl = asset.browser_download_url; - } else { - // if no asset name specified, try to find a .js file or use the first asset - const jsAsset = releaseData.assets.find((a) => a.name.endsWith(".js")); - if (!jsAsset) { - if (releaseData.assets.length === 0) { - throw new Error(`No assets found in release ${releaseData.tag_name}`); - } - assetUrl = releaseData.assets[0].browser_download_url; - } else { - assetUrl = jsAsset.browser_download_url; - } + const asset = releaseData.assets.find((a) => a.name === assetName); + if (!asset) { + throw new Error(`Asset '${assetName}' not found in release ${releaseData.tag_name}`); } + const assetUrl = asset.browser_download_url; log.info(`Downloading asset from ${assetUrl}...`); @@ -267,41 +252,7 @@ export async function installFromGithub({ // determine the executable path let cliPath: string; if (executablePath) { - // if executablePath is provided, assume the downloaded file needs to be extracted - // and the executable is inside - if (fileName.endsWith(".tar.gz") || fileName.endsWith(".tgz")) { - // extract tarball - log.info(`Extracting tarball...`); - const extractResult = spawnSync("tar", ["-xzf", downloadPath, "-C", tempDir], { - stdio: "pipe", - encoding: "utf-8", - }); - if (extractResult.status !== 0) { - throw new Error( - `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}` - ); - } - // find the extracted directory (usually named after the repo) - const extractedDir = join(tempDir, repo); - cliPath = join(extractedDir, executablePath); - } else if (fileName.endsWith(".zip")) { - // extract zip - log.info(`Extracting zip...`); - const extractResult = spawnSync("unzip", ["-q", downloadPath, "-d", tempDir], { - stdio: "pipe", - encoding: "utf-8", - }); - if (extractResult.status !== 0) { - throw new Error( - `Failed to extract zip: ${extractResult.stderr || extractResult.stdout || "Unknown error"}` - ); - } - const extractedDir = join(tempDir, repo); - cliPath = join(extractedDir, executablePath); - } else { - // assume it's a single file, executablePath is relative to temp dir - cliPath = join(tempDir, executablePath); - } + cliPath = join(tempDir, executablePath); } else { // no executablePath, assume the downloaded file is the executable cliPath = downloadPath; From ed39bda62abe4335f24d2c3a789e22777b1afcf7 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Thu, 20 Nov 2025 14:04:47 -0500 Subject: [PATCH 3/6] sketchy remove --- agents/cursor.ts | 11 +-------- agents/gemini.ts | 4 ++-- agents/shared.ts | 2 ++ fixtures/basic.txt | 2 +- mcp/server.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++ mcp/shared.ts | 29 +++++++++++++++++++++++- 6 files changed, 90 insertions(+), 14 deletions(-) diff --git a/agents/cursor.ts b/agents/cursor.ts index 2ac7b6f..04be952 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -21,24 +21,18 @@ export const cursor = agent({ configureCursorMcpServers({ mcpServers, cliPath }); try { - // Run cursor-agent in non-interactive mode with the prompt - // Using -p flag for prompt, --output-format text for plain text output - // and --approve-mcps to automatically approve all MCP servers const fullPrompt = addInstructions(payload); - // Find temp directory from cliPath to set HOME for MCP config lookup const tempDir = cliPath.split("/.local/bin/")[0]; log.info("Running Cursor CLI..."); - // Use spawn to handle streaming output - // Use --print flag explicitly for non-interactive mode return new Promise((resolve) => { const child = spawn( cliPath, ["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"], { - cwd: process.cwd(), // Run in current working directory + cwd: process.cwd(), env: { ...process.env, CURSOR_API_KEY: apiKey, @@ -52,7 +46,6 @@ export const cursor = agent({ let stdout = ""; let stderr = ""; - // Log when process starts child.on("spawn", () => { log.debug("Cursor CLI process spawned"); }); @@ -67,12 +60,10 @@ export const cursor = agent({ child.stderr?.on("data", (data) => { const text = data.toString(); stderr += text; - // Log errors as they come - but also write to stdout so we can see it process.stderr.write(text); log.warning(text); }); - // Handle process exit child.on("close", (code, signal) => { if (signal) { log.warning(`Cursor CLI terminated by signal: ${signal}`); diff --git a/agents/gemini.ts b/agents/gemini.ts index 84a826b..299fdbc 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -32,10 +32,11 @@ export const gemini = agent({ try { const result = await spawn({ cmd: "node", - args: [cliPath, "--yolo", "--output-format", "text", "-p", sessionPrompt], + args: [cliPath, "--yolo", "--output-format=text", "-p", sessionPrompt], env: { GEMINI_API_KEY: apiKey, GITHUB_INSTALLATION_TOKEN: githubInstallationToken, + GEMINI_CLI_DISABLE_SCHEMA_VALIDATION: "1", }, onStdout: (chunk) => { const trimmed = chunk.trim(); @@ -94,7 +95,6 @@ function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersP const addArgs = ["mcp", "add", serverName, command, ...args]; - // Add environment variables as --env flags for (const [key, value] of Object.entries(envVars)) { addArgs.push("--env", `${key}=${value}`); } diff --git a/agents/shared.ts b/agents/shared.ts index 754d392..0ad584b 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -1,5 +1,7 @@ import { spawnSync } from "node:child_process"; import { chmodSync, createWriteStream, existsSync } from "node:fs"; +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { pipeline } from "node:stream/promises"; import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; diff --git a/fixtures/basic.txt b/fixtures/basic.txt index 48607c8..6d14410 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -create a comment on https://github.com/pullfrogai/scratch/pull/29 that says Things just got out of hand \ No newline at end of file +comment on https://github.com/pullfrogai/scratch/pull/29 that writes a really funny joke about Arktype \ No newline at end of file diff --git a/mcp/server.ts b/mcp/server.ts index 96e68e0..d650b2e 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -36,4 +36,60 @@ addTools(server, [ GetCheckSuiteLogsTool, ]); +// intercept stdout to remove $schema fields from JSON-RPC messages +// FastMCP uses stdout for JSON-RPC communication, so we intercept here +const originalStdoutWrite = process.stdout.write.bind(process.stdout); +process.stdout.write = function (chunk: any, encoding?: any, cb?: any): boolean { + if (typeof chunk === "string" || Buffer.isBuffer(chunk)) { + try { + const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8"); + // MCP uses JSON-RPC over stdio, messages are JSON objects on single lines + const lines = text.split("\n"); + const modifiedLines = lines.map((line) => { + const trimmed = line.trim(); + // only process lines that look like JSON-RPC messages + if (trimmed.startsWith("{") && trimmed.includes('"jsonrpc"')) { + try { + const parsed = JSON.parse(trimmed); + // recursively remove $schema fields from the message + const cleaned = removeSchemaFields(parsed); + return JSON.stringify(cleaned) + "\n"; + } catch { + // if parsing fails, return original line + return line + (line.endsWith("\n") ? "" : "\n"); + } + } + return line + (line.endsWith("\n") ? "" : "\n"); + }); + const result = modifiedLines.join(""); + return originalStdoutWrite(result, encoding, cb); + } catch { + // if anything fails, just pass through + } + } + return originalStdoutWrite(chunk, encoding, cb); +}; + +// recursively remove $schema fields from JSON objects +function removeSchemaFields(obj: any): any { + if (obj === null || typeof obj !== "object") { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(removeSchemaFields); + } + + const result: any = {}; + for (const [key, value] of Object.entries(obj)) { + // skip $schema fields + if (key === "$schema") { + continue; + } + result[key] = removeSchemaFields(value); + } + + return result; +} + server.start(); diff --git a/mcp/shared.ts b/mcp/shared.ts index d86490d..79ae24d 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -65,9 +65,36 @@ export const tool = (toolDef: Tool>) return toolDef; }; +// recursively remove $schema fields from JSON Schema objects +function removeSchemaFields(obj: any): any { + if (obj === null || typeof obj !== "object") { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(removeSchemaFields); + } + + const result: any = {}; + for (const [key, value] of Object.entries(obj)) { + // skip $schema fields + if (key === "$schema") { + continue; + } + result[key] = removeSchemaFields(value); + } + + return result; +} + export const addTools = (server: FastMCP, tools: Tool[]) => { for (const tool of tools) { - server.addTool(tool); + // clone tool and remove $schema from parameters schema + const cleanedTool = { + ...tool, + parameters: tool.parameters ? removeSchemaFields(tool.parameters) : undefined, + }; + server.addTool(cleanedTool); } return server; }; From 8944e7fe08084840dce409caa4e2d981034113c5 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Fri, 21 Nov 2025 11:06:37 -0500 Subject: [PATCH 4/6] . --- agents/gemini.ts | 1 - fixtures/basic.txt | 2 +- mcp/arkConfig.ts | 2 +- mcp/shared.ts | 29 +---------------------------- play.ts | 2 +- 5 files changed, 4 insertions(+), 32 deletions(-) diff --git a/agents/gemini.ts b/agents/gemini.ts index 4c872cd..ded54d9 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -35,7 +35,6 @@ export const gemini = agent({ env: { GEMINI_API_KEY: apiKey, GITHUB_INSTALLATION_TOKEN: githubInstallationToken, - GEMINI_CLI_DISABLE_SCHEMA_VALIDATION: "1", }, onStdout: (chunk) => { const trimmed = chunk.trim(); diff --git a/fixtures/basic.txt b/fixtures/basic.txt index 6d14410..b9fcc52 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -comment on https://github.com/pullfrogai/scratch/pull/29 that writes a really funny joke about Arktype \ No newline at end of file +comment on https://github.com/pullfrogai/scratch/pull/29 that writes a really funny joke about Codex \ No newline at end of file diff --git a/mcp/arkConfig.ts b/mcp/arkConfig.ts index 8128d85..c9cfb15 100644 --- a/mcp/arkConfig.ts +++ b/mcp/arkConfig.ts @@ -2,6 +2,6 @@ import { configure } from "arktype/config"; configure({ toJsonSchema: { - dialect: null, + // dialect: null, }, }); diff --git a/mcp/shared.ts b/mcp/shared.ts index 79ae24d..d86490d 100644 --- a/mcp/shared.ts +++ b/mcp/shared.ts @@ -65,36 +65,9 @@ export const tool = (toolDef: Tool>) return toolDef; }; -// recursively remove $schema fields from JSON Schema objects -function removeSchemaFields(obj: any): any { - if (obj === null || typeof obj !== "object") { - return obj; - } - - if (Array.isArray(obj)) { - return obj.map(removeSchemaFields); - } - - const result: any = {}; - for (const [key, value] of Object.entries(obj)) { - // skip $schema fields - if (key === "$schema") { - continue; - } - result[key] = removeSchemaFields(value); - } - - return result; -} - export const addTools = (server: FastMCP, tools: Tool[]) => { for (const tool of tools) { - // clone tool and remove $schema from parameters schema - const cleanedTool = { - ...tool, - parameters: tool.parameters ? removeSchemaFields(tool.parameters) : undefined, - }; - server.addTool(cleanedTool); + server.addTool(tool); } return server; }; diff --git a/play.ts b/play.ts index 9ba3a57..c7b7a26 100644 --- a/play.ts +++ b/play.ts @@ -24,7 +24,7 @@ export async function run( const inputs: Required = { prompt, - agent: "gemini", + agent: "codex", ...flatMorph(agents, (_, agent) => agent.apiKeyNames.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) ), From 589592372faad31eecbaeb895eb4f2b6673f2270 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Fri, 21 Nov 2025 14:00:12 -0500 Subject: [PATCH 5/6] github token --- fixtures/basic.txt | 4 +--- main.ts | 11 +++-------- play.ts | 2 +- utils/github.ts | 21 ++------------------- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/fixtures/basic.txt b/fixtures/basic.txt index 117d20a..482eeb8 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1,3 +1 @@ -Print the MCP tools available to you. -Try to call select_mode with the name "Plan". -Then tell me a joke \ No newline at end of file +write a comment to https://github.com/pullfrogai/scratch/pull/29 that tells a joke \ No newline at end of file diff --git a/main.ts b/main.ts index 9127a99..13dac65 100644 --- a/main.ts +++ b/main.ts @@ -135,7 +135,6 @@ To fix this, add the required secret to your GitHub repository: interface MainContext { inputs: Inputs; githubInstallationToken: string; - tokenToRevoke: string | null; repoContext: RepoContext; agentName: AgentNameType; agent: (typeof agents)[AgentNameType]; @@ -155,16 +154,14 @@ async function initializeContext( Inputs.assert(inputs); setupGitConfig(); - const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken(); - const tokenToRevoke = wasAcquired ? githubInstallationToken : null; + const githubInstallationToken = await setupGitHubInstallationToken(); const repoContext = parseRepoContext(); return { inputs, githubInstallationToken, - tokenToRevoke, repoContext, - agentName: "claude" as AgentNameType, + agentName: "claude", agent: agents.claude, sharedTempDir: "", mcpLogPath: "", @@ -292,7 +289,5 @@ async function cleanup(ctx: Omit { // we don't need to extract it here since main() will parse the payload const inputs: Required = { prompt, - defaultAgent: "cursor", + defaultAgent: "gemini", ...flatMorph(agents, (_, agent) => agent.apiKeyNames.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) ), diff --git a/utils/github.ts b/utils/github.ts index 98fb24f..f1649a5 100644 --- a/utils/github.ts +++ b/utils/github.ts @@ -43,12 +43,6 @@ interface RepositoriesResponse { repositories: Repository[]; } -function checkExistingToken(): string | null { - const inputToken = core.getInput("github_installation_token"); - const envToken = process.env.GITHUB_INSTALLATION_TOKEN; - return inputToken || envToken || null; -} - function isGitHubActionsEnvironment(): boolean { return Boolean(process.env.GITHUB_ACTIONS); } @@ -249,24 +243,13 @@ async function acquireNewToken(): Promise { /** * Setup GitHub installation token for the action - * Returns the token and whether it was acquired (needs revocation) */ -export async function setupGitHubInstallationToken(): Promise<{ - githubInstallationToken: string; - wasAcquired: boolean; -}> { - const existingToken = checkExistingToken(); - if (existingToken) { - core.setSecret(existingToken); - log.info("Using provided GitHub installation token"); - return { githubInstallationToken: existingToken, wasAcquired: false }; - } - +export async function setupGitHubInstallationToken(): Promise { const acquiredToken = await acquireNewToken(); core.setSecret(acquiredToken); process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken; - return { githubInstallationToken: acquiredToken, wasAcquired: true }; + return acquiredToken; } /** From 264dcc072c8a142f2e4ce5061eb5e311ef4ed15c Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Fri, 21 Nov 2025 14:08:36 -0500 Subject: [PATCH 6/6] remove stdout interception logic --- mcp/arkConfig.ts | 2 +- mcp/server.ts | 56 ------------------------------------------------ 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/mcp/arkConfig.ts b/mcp/arkConfig.ts index c9cfb15..8128d85 100644 --- a/mcp/arkConfig.ts +++ b/mcp/arkConfig.ts @@ -2,6 +2,6 @@ import { configure } from "arktype/config"; configure({ toJsonSchema: { - // dialect: null, + dialect: null, }, }); diff --git a/mcp/server.ts b/mcp/server.ts index 9917252..cfa8ab8 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -37,60 +37,4 @@ addTools(server, [ GetCheckSuiteLogsTool, ]); -// intercept stdout to remove $schema fields from JSON-RPC messages -// FastMCP uses stdout for JSON-RPC communication, so we intercept here -const originalStdoutWrite = process.stdout.write.bind(process.stdout); -process.stdout.write = function (chunk: any, encoding?: any, cb?: any): boolean { - if (typeof chunk === "string" || Buffer.isBuffer(chunk)) { - try { - const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8"); - // MCP uses JSON-RPC over stdio, messages are JSON objects on single lines - const lines = text.split("\n"); - const modifiedLines = lines.map((line) => { - const trimmed = line.trim(); - // only process lines that look like JSON-RPC messages - if (trimmed.startsWith("{") && trimmed.includes('"jsonrpc"')) { - try { - const parsed = JSON.parse(trimmed); - // recursively remove $schema fields from the message - const cleaned = removeSchemaFields(parsed); - return JSON.stringify(cleaned) + "\n"; - } catch { - // if parsing fails, return original line - return line + (line.endsWith("\n") ? "" : "\n"); - } - } - return line + (line.endsWith("\n") ? "" : "\n"); - }); - const result = modifiedLines.join(""); - return originalStdoutWrite(result, encoding, cb); - } catch { - // if anything fails, just pass through - } - } - return originalStdoutWrite(chunk, encoding, cb); -}; - -// recursively remove $schema fields from JSON objects -function removeSchemaFields(obj: any): any { - if (obj === null || typeof obj !== "object") { - return obj; - } - - if (Array.isArray(obj)) { - return obj.map(removeSchemaFields); - } - - const result: any = {}; - for (const [key, value] of Object.entries(obj)) { - // skip $schema fields - if (key === "$schema") { - continue; - } - result[key] = removeSchemaFields(value); - } - - return result; -} - server.start();