Skip to main content
Glama

Git MCP Server

runtime-adapter.ts9.73 kB
/** * @fileoverview Runtime adapter for cross-runtime process spawning * @module services/git/providers/cli/utils/runtime-adapter * * This module provides a unified interface for spawning git processes * that works in both Bun and Node.js runtimes. When running via bunx * (Node.js), it uses child_process.spawn. When running in native Bun, * it uses Bun.spawn for better performance and security. * * ## Why eslint-disable is necessary * * TypeScript cannot infer types for `globalThis.Bun` at compile time because: * 1. The Bun types are only available when running in Bun runtime * 2. This code must compile for both Node.js and Bun targets * 3. We must dynamically access `globalThis.Bun` and cast it * * We minimize unsafe code by: * - Creating a typed interface for only the Bun APIs we use * - Isolating the single `any` cast to the initial access * - Using TypeScript interfaces for all downstream usage */ import { spawn } from 'node:child_process'; /** * Result of executing a git command. */ export interface GitCommandResult { /** Standard output from the command */ stdout: string; /** Standard error from the command */ stderr: string; } /** * Minimal typed interface for Bun's spawn API. * * This interface includes only the subset of Bun's API that we actually use, * providing type safety for the dynamic access to globalThis.Bun. * * @internal */ interface BunSpawnAPI { spawn( cmd: string[], options: { cwd: string; env: Record<string, string>; stdio: [string, string, string]; }, ): BunSubprocess; } /** * Bun extends ReadableStream with additional helper methods. * This interface represents Bun's enhanced ReadableStream. * * @internal */ interface BunReadableStream extends ReadableStream<Uint8Array> { /** * Bun-specific extension: Read the entire stream as text. * This is more efficient than using a TextDecoder manually. */ text(): Promise<string>; } /** * Minimal typed interface for Bun's Subprocess. * * @internal */ interface BunSubprocess { readonly stdout: BunReadableStream; readonly stderr: BunReadableStream; readonly exited: Promise<number>; kill(): void; } /** * Detects the current JavaScript runtime. * * @returns 'bun' if running in Bun, 'node' if running in Node.js * * @example * ```typescript * const runtime = detectRuntime(); * if (runtime === 'bun') { * // Use Bun-specific APIs * } else { * // Use Node.js APIs * } * ``` */ export function detectRuntime(): 'bun' | 'node' { // Check for Bun global object (most reliable) if (typeof globalThis.Bun !== 'undefined') { return 'bun'; } // Check process.versions.bun as fallback if (process.versions?.bun) { return 'bun'; } return 'node'; } /** * Spawns a git command using Bun.spawn for optimal performance. * * This function is used when running in native Bun runtime. It uses Bun's * native spawn API which provides better performance than Node's child_process. * * The function: * 1. Spawns the git process with piped stdout/stderr * 2. Races the process exit against a timeout and abort signal * 3. Reads streams using the standard ReadableStream.text() method * 4. Returns structured output or throws on error * * @param args - Git command arguments * @param cwd - Working directory * @param env - Environment variables * @param timeout - Timeout in milliseconds * @param signal - Optional AbortSignal for cancellation * @returns Promise resolving to stdout and stderr * @throws Error if the command fails, times out, or is aborted */ async function spawnWithBun( args: string[], cwd: string, env: Record<string, string>, timeout: number, signal?: AbortSignal, ): Promise<GitCommandResult> { // Cast globalThis.Bun to our typed interface // eslint-disable-next-line @typescript-eslint/no-explicit-any const bunApi = globalThis.Bun as any as BunSpawnAPI; // Check if already aborted before starting if (signal?.aborted) { throw new Error( `Git command cancelled before execution: git ${args.join(' ')}`, ); } // Spawn the process using typed interface - no more eslint-disable needed const proc = bunApi.spawn(['git', ...args], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], }); // Create abort signal listener that kills the process const abortPromise = new Promise<never>((_, reject) => { if (signal) { signal.addEventListener( 'abort', () => { proc.kill(); reject(new Error(`Git command cancelled: git ${args.join(' ')}`)); }, { once: true }, ); } }); // Create a timeout promise that will kill the process if it exceeds the limit const timeoutPromise = new Promise<never>((_, reject) => { const timeoutId = setTimeout(() => { proc.kill(); reject( new Error( `Git command timed out after ${timeout / 1000}s: git ${args.join(' ')}`, ), ); }, timeout); // Ensure timeout is cleared if process exits normally void proc.exited.finally(() => clearTimeout(timeoutId)); }); // Wait for the process to exit, racing against timeout and abort signal const exitCode = await Promise.race([ proc.exited, timeoutPromise, ...(signal ? [abortPromise] : []), ]); // Read the output streams using standard ReadableStream.text() method // This is the modern Web Streams API approach const [stdout, stderr] = await Promise.all([ proc.stdout.text(), proc.stderr.text(), ]); // Check if the command succeeded (exit code 0) if (exitCode !== 0) { const combinedOutput = `Exit Code: ${exitCode}\nStderr: ${stderr}\nStdout: ${stdout}`; throw new Error(combinedOutput); } return { stdout, stderr }; } /** * Spawns a git command using Node.js child_process.spawn. * * This function is used when running via bunx or in Node.js runtime. * * @param args - Git command arguments * @param cwd - Working directory * @param env - Environment variables * @param timeout - Timeout in milliseconds * @param signal - Optional AbortSignal for cancellation * @returns Promise resolving to stdout and stderr * @throws Error if the command fails, times out, or is aborted */ async function spawnWithNode( args: string[], cwd: string, env: Record<string, string>, timeout: number, signal?: AbortSignal, ): Promise<GitCommandResult> { return new Promise((resolve, reject) => { // Check if already aborted before starting if (signal?.aborted) { reject( new Error(`Git command cancelled before execution: ${args.join(' ')}`), ); return; } const proc = spawn('git', args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], }); const stdoutChunks: Buffer[] = []; const stderrChunks: Buffer[] = []; proc.stdout.on('data', (chunk: Buffer) => { stdoutChunks.push(chunk); }); proc.stderr.on('data', (chunk: Buffer) => { stderrChunks.push(chunk); }); // Setup abort signal handler const abortHandler = () => { proc.kill('SIGTERM'); reject(new Error(`Git command cancelled: ${args.join(' ')}`)); }; if (signal) { signal.addEventListener('abort', abortHandler, { once: true }); } // Setup timeout const timeoutHandle = setTimeout(() => { proc.kill('SIGTERM'); reject( new Error( `Git command timed out after ${timeout / 1000}s: ${args.join(' ')}`, ), ); }, timeout); proc.on('error', (error) => { clearTimeout(timeoutHandle); if (signal) { signal.removeEventListener('abort', abortHandler); } reject(error); }); proc.on('close', (exitCode) => { clearTimeout(timeoutHandle); if (signal) { signal.removeEventListener('abort', abortHandler); } const stdout = Buffer.concat(stdoutChunks).toString('utf-8'); const stderr = Buffer.concat(stderrChunks).toString('utf-8'); if (exitCode !== 0) { const combinedOutput = `Exit Code: ${exitCode}\nStderr: ${stderr}\nStdout: ${stdout}`; reject(new Error(combinedOutput)); } else { resolve({ stdout, stderr }); } }); }); } /** * Spawns a git command using the appropriate runtime implementation. * * Automatically detects the runtime (Bun vs Node.js) and uses the * optimal process spawning method for that runtime. * * Supports cancellation via AbortSignal (MCP Spec 2025-06-18): * - Clients can cancel long-running operations by aborting the request * - The git process will be killed and resources cleaned up * * @param args - Git command arguments (e.g., ['status', '--porcelain']) * @param cwd - Working directory for command execution * @param env - Environment variables * @param timeout - Timeout in milliseconds (default: 60000) * @param signal - Optional AbortSignal for cancellation support * @returns Promise resolving to stdout and stderr * @throws Error if the command fails, times out, or is cancelled * * @example * ```typescript * const result = await spawnGitCommand( * ['status', '--porcelain'], * '/path/to/repo', * { GIT_TERMINAL_PROMPT: '0' }, * 60000, * abortController.signal * ); * console.log(result.stdout); // Git output * ``` */ export async function spawnGitCommand( args: string[], cwd: string, env: Record<string, string>, timeout = 60000, signal?: AbortSignal, ): Promise<GitCommandResult> { const runtime = detectRuntime(); if (runtime === 'bun') { return spawnWithBun(args, cwd, env, timeout, signal); } else { return spawnWithNode(args, cwd, env, timeout, signal); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/git-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server