Skip to main content
Glama
sessions.ts11.7 kB
// Interactive process sessions // Allows starting, interacting with, and managing long-running processes import { z } from 'zod'; import { spawn, ChildProcess } from 'child_process'; import { logAudit } from '../audit.js'; import { loadConfig } from '../config.js'; import os from 'os'; const platform = os.platform(); const config = loadConfig(); // Session storage interface ProcessSession { id: string; command: string; cwd: string; process: ChildProcess; output: string[]; startTime: Date; isAlive: boolean; } const sessions: Map<string, ProcessSession> = new Map(); const MAX_OUTPUT_LINES = 1000; // Keep last 1000 lines per session // Generate unique session ID function generateSessionId(): string { return `session_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; } // Schemas export const StartProcessSchema = { command: z.string().describe('Command to execute'), args: z.array(z.string()).optional().describe('Command arguments'), cwd: z.string().optional().describe('Working directory'), env: z.record(z.string()).optional().describe('Environment variables to set'), }; export const InteractWithProcessSchema = { sessionId: z.string().describe('Session ID returned from start_process'), input: z.string().describe('Input to send to the process stdin'), }; export const ReadProcessOutputSchema = { sessionId: z.string().describe('Session ID returned from start_process'), lines: z.number().optional().describe('Number of lines to return (default: all available, negative for last N lines)'), clear: z.boolean().optional().describe('Clear output buffer after reading (default: false)'), }; export const ListSessionsSchema = {}; export const TerminateProcessSchema = { sessionId: z.string().describe('Session ID to terminate'), force: z.boolean().optional().describe('Force kill (SIGKILL) instead of graceful (SIGTERM)'), }; /** * Start a new interactive process session */ export async function handleStartProcess(args: { command: string; args?: string[]; cwd?: string; env?: Record<string, string>; }): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> { try { const sessionId = generateSessionId(); const cmdArgs = args.args || []; const cwd = args.cwd || process.cwd(); // Spawn the process const child = spawn(args.command, cmdArgs, { cwd, env: { ...process.env, ...args.env }, shell: platform === 'win32', // Use shell on Windows for better compatibility stdio: ['pipe', 'pipe', 'pipe'], }); const session: ProcessSession = { id: sessionId, command: `${args.command} ${cmdArgs.join(' ')}`.trim(), cwd, process: child, output: [], startTime: new Date(), isAlive: true, }; // Collect stdout child.stdout?.on('data', (data: Buffer) => { const lines = data.toString().split('\n'); session.output.push(...lines.filter(l => l.length > 0)); // Trim to max lines if (session.output.length > MAX_OUTPUT_LINES) { session.output = session.output.slice(-MAX_OUTPUT_LINES); } }); // Collect stderr child.stderr?.on('data', (data: Buffer) => { const lines = data.toString().split('\n').map(l => `[stderr] ${l}`); session.output.push(...lines.filter(l => l.length > 8)); // > "[stderr] ".length if (session.output.length > MAX_OUTPUT_LINES) { session.output = session.output.slice(-MAX_OUTPUT_LINES); } }); // Handle process exit child.on('exit', (code, signal) => { session.isAlive = false; session.output.push(`[process exited with ${signal ? `signal ${signal}` : `code ${code}`}]`); }); child.on('error', (err) => { session.isAlive = false; session.output.push(`[process error: ${err.message}]`); }); sessions.set(sessionId, session); await logAudit('start_process', { command: args.command, args: cmdArgs, cwd }, { sessionId, pid: child.pid }); return { content: [{ type: 'text', text: JSON.stringify({ sessionId, pid: child.pid, command: session.command, cwd, started: session.startTime.toISOString(), }, null, 2) }], }; } catch (error: any) { await logAudit('start_process', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } } /** * Send input to a running process */ export async function handleInteractWithProcess(args: { sessionId: string; input: string; }): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> { try { const session = sessions.get(args.sessionId); if (!session) { return { content: [{ type: 'text', text: `Error: Session not found: ${args.sessionId}` }], isError: true, }; } if (!session.isAlive) { return { content: [{ type: 'text', text: `Error: Process has exited` }], isError: true, }; } // Write to stdin session.process.stdin?.write(args.input); // If input doesn't end with newline, add one (common for commands) if (!args.input.endsWith('\n')) { session.process.stdin?.write('\n'); } await logAudit('interact_with_process', { sessionId: args.sessionId, inputLength: args.input.length }, 'sent'); return { content: [{ type: 'text', text: JSON.stringify({ sessionId: args.sessionId, sent: true, inputLength: args.input.length, }, null, 2) }], }; } catch (error: any) { await logAudit('interact_with_process', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } } /** * Read output from a process session */ export async function handleReadProcessOutput(args: { sessionId: string; lines?: number; clear?: boolean; }): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> { try { const session = sessions.get(args.sessionId); const outputConfig = config.cliOutput ?? { maxOutputChars: 50000, warnAtChars: 10000, truncateMode: 'both' as const }; const maxReturnLines = 200; // Default limit on returned lines to prevent context stuffing if (!session) { return { content: [{ type: 'text', text: `Error: Session not found: ${args.sessionId}` }], isError: true, }; } let output: string[]; let requestedLines = args.lines; // Apply default limit if not specified if (requestedLines === undefined) { requestedLines = -maxReturnLines; // Default to last N lines } if (requestedLines < 0) { // Negative: last N lines (cap at maxReturnLines) const linesToGet = Math.min(Math.abs(requestedLines), maxReturnLines); output = session.output.slice(-linesToGet); } else { // Positive: first N lines (cap at maxReturnLines) const linesToGet = Math.min(requestedLines, maxReturnLines); output = session.output.slice(0, linesToGet); } if (args.clear) { session.output = []; } const outputText = output.join('\n'); const truncatedByLines = output.length < session.output.length; // Also check character limit let finalOutput = outputText; let truncatedByChars = false; if (outputText.length > outputConfig.maxOutputChars) { finalOutput = outputText.slice(0, outputConfig.maxOutputChars); finalOutput += `\n\n⚠️ OUTPUT TRUNCATED: ${outputText.length.toLocaleString()} chars exceeded limit of ${outputConfig.maxOutputChars.toLocaleString()}`; truncatedByChars = true; } await logAudit('read_process_output', { sessionId: args.sessionId, linesRequested: args.lines }, { linesReturned: output.length, truncated: truncatedByLines || truncatedByChars }); return { content: [{ type: 'text', text: JSON.stringify({ sessionId: args.sessionId, isAlive: session.isAlive, linesReturned: output.length, totalBuffered: session.output.length, truncated: truncatedByLines || truncatedByChars, output: finalOutput, }, null, 2) }], }; } catch (error: any) { await logAudit('read_process_output', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } } /** * List all active sessions */ export async function handleListSessions(): Promise<{ content: Array<{ type: string; text: string }> }> { const sessionList = Array.from(sessions.values()).map(s => ({ sessionId: s.id, command: s.command, cwd: s.cwd, pid: s.process.pid, isAlive: s.isAlive, startTime: s.startTime.toISOString(), outputLines: s.output.length, })); await logAudit('list_sessions', {}, { count: sessionList.length }); return { content: [{ type: 'text', text: JSON.stringify({ count: sessionList.length, sessions: sessionList, }, null, 2) }], }; } /** * Terminate a process session */ export async function handleTerminateProcess(args: { sessionId: string; force?: boolean; }): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> { try { const session = sessions.get(args.sessionId); if (!session) { return { content: [{ type: 'text', text: `Error: Session not found: ${args.sessionId}` }], isError: true, }; } if (session.isAlive) { if (args.force) { session.process.kill('SIGKILL'); } else { session.process.kill('SIGTERM'); } } // Give it a moment then clean up setTimeout(() => { sessions.delete(args.sessionId); }, 1000); await logAudit('terminate_process', args, 'terminated'); return { content: [{ type: 'text', text: JSON.stringify({ sessionId: args.sessionId, terminated: true, wasAlive: session.isAlive, force: args.force || false, }, null, 2) }], }; } catch (error: any) { await logAudit('terminate_process', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } }

Latest Blog Posts

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/Mnehmos/mnehmos.ooda.mcp'

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