Skip to main content
Glama
index.ts6.15 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { createReadStream, watch } from "fs"; import { access } from "fs/promises"; import { dirname, basename } from "path"; import { exec } from "child_process"; import { promisify } from "util"; const execAsync = promisify(exec); interface NodeError extends Error { code?: string; } interface LogEntry { timestamp: string; message: string; } const PIPE_NAME = process.platform === "win32" ? "\\\\.\\pipe\\stdout_pipe" // Windows named pipe in standard location : "/tmp/stdout_pipe"; // Unix pipe in /tmp directory const MAX_STORED_LOGS = 100; // Store logs in memory as structured objects const logStore: LogEntry[] = []; // Helper function to log in JSON format function logJson(message: string): void { const entry: LogEntry = { timestamp: new Date().toISOString(), message, }; console.log(JSON.stringify(entry)); } // Create server instance const server = new McpServer({ name: "stdout-mcp-server", version: "1.0.0", }); /** * Create a named pipe. */ async function createNamedPipe(): Promise<void> { try { // Check if pipe already exists try { await access(PIPE_NAME); logJson("Named pipe already exists"); return; } catch { // Pipe doesn't exist, continue to create it } if (process.platform === "win32") { // Windows named pipe creation using PowerShell await execAsync( `powershell.exe -Command "New-Item -ItemType NamedPipe -Path '${PIPE_NAME}'"`, ); } else { // Unix named pipe creation using mkfifo await execAsync(`mkfifo ${PIPE_NAME}`); } logJson(`Created named pipe at ${PIPE_NAME}`); } catch (error) { logJson(`Failed to create named pipe: ${error}`); throw error; } } /** * Start a file watcher that reads from the named pipe and logs the data to the console. */ async function startFileWatcher(): Promise<void> { try { await createNamedPipe(); let buffer = ""; let currentStream: ReturnType<typeof createReadStream> | null = null; let isReading = false; function startReading(): void { if (currentStream || isReading) return; try { isReading = true; currentStream = createReadStream(PIPE_NAME, { encoding: "utf8" }); logJson("Started reading from log pipe"); currentStream.on("data", (data) => { buffer += data; // Process complete lines const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.trim()) { // Just store the raw message with timestamp const entry: LogEntry = { timestamp: new Date().toISOString(), message: line.trim(), }; logStore.push(entry); console.log(JSON.stringify(entry)); if (logStore.length > MAX_STORED_LOGS) { logStore.shift(); } } } }); currentStream.on("error", (error: NodeError) => { if (error.code === "ENOENT") { logJson("Waiting for named pipe to be created..."); } else { logJson(`Error reading from pipe: ${error}`); } currentStream = null; isReading = false; }); currentStream.on("end", () => { logJson("Pipe read stream ended"); currentStream = null; isReading = false; // Don't automatically reconnect - wait for the next write event // The watcher will handle reconnection when new data is available }); } catch (error) { logJson(`Failed to create read stream: ${error}`); currentStream = null; isReading = false; } } // Watch for file changes const watcher = watch(dirname(PIPE_NAME), (eventType, filename) => { if (filename === basename(PIPE_NAME) && !isReading) { // Only start reading if we're not already reading startReading(); } }); watcher.on("error", (error) => { logJson(`Watch error: ${error}`); }); logJson(`Watching for logs at ${PIPE_NAME}`); // Initial read startReading(); } catch (error) { logJson(`Failed to start file watcher: ${error}`); process.exit(1); } } // Register get-logs tool server.tool( "get-logs", "Retrieve logs from the named pipe with optional filtering", { lines: z .number() .optional() .default(50) .describe("Number of log lines to return"), filter: z.string().optional().describe("Text to filter logs by"), since: z.number().optional().describe("Timestamp to get logs after"), }, async ({ lines, filter, since }) => { try { let logs = [...logStore]; if (filter) { logs = logs.filter((entry) => entry.message.toLowerCase().includes(filter.toLowerCase()), ); } if (since) { logs = logs.filter((entry) => { const timestamp = new Date(entry.timestamp).getTime(); return timestamp > since; }); } // Take the last N lines and reverse them so oldest is first logs = logs.slice(-lines).reverse(); return { content: [ { type: "text", text: JSON.stringify(logs, null, 2), }, ], }; } catch (error) { logJson(`Error retrieving logs: ${error}`); throw new Error("Failed to retrieve logs"); } }, ); // Main function async function main(): Promise<void> { try { await createNamedPipe(); await startFileWatcher(); const transport = new StdioServerTransport(); await server.connect(transport); logJson("Pipe log MCP server started"); } catch (error) { logJson(`Failed to start server: ${error}`); throw error; } } main().catch((error) => { logJson(`Fatal error in main(): ${error}`); process.exit(1); });

Implementation Reference

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/amitdeshmukh/stdout-mcp-server'

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