#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { readFileSync, existsSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
// Log file path - stores captured terminal output
const LOG_FILE = process.env.TERMINAL_LOG_FILE || join(homedir(), '.terminal_history.log');
// Create the MCP server
const server = new McpServer({
name: 'terminal-reader-mcp',
version: '1.0.0',
});
/**
* Parse the log file into individual command entries
* Log format:
* ---CMD---
* $ command here
* ---OUTPUT---
* output lines here
* ---EXIT:0---
* ---END---
*/
function parseLogFile() {
if (!existsSync(LOG_FILE)) {
return [];
}
const content = readFileSync(LOG_FILE, 'utf8');
const entries = [];
const blocks = content.split('---CMD---').filter(block => block.trim());
for (const block of blocks) {
const entry = {
command: '',
output: '',
exitCode: null,
timestamp: null,
};
// Extract command (line starting with $)
const cmdMatch = block.match(/^\s*\$\s*(.+?)(?:\n|---)/m);
if (cmdMatch) {
entry.command = cmdMatch[1].trim();
}
// Extract output
const outputMatch = block.match(/---OUTPUT---\n([\s\S]*?)(?:---EXIT|---END|$)/);
if (outputMatch) {
entry.output = outputMatch[1].trim();
}
// Extract exit code
const exitMatch = block.match(/---EXIT:(\d+)---/);
if (exitMatch) {
entry.exitCode = parseInt(exitMatch[1], 10);
}
// Extract timestamp if present
const timestampMatch = block.match(/---TIMESTAMP:(.+?)---/);
if (timestampMatch) {
entry.timestamp = timestampMatch[1];
}
if (entry.command) {
entries.push(entry);
}
}
return entries;
}
// Tool: Get the last command and its output
server.registerTool(
'get_last_command',
{
title: 'Get Last Command',
description: 'Get the most recent terminal command and its output. Use this to see what the user just ran in their terminal.',
inputSchema: {},
},
async () => {
const entries = parseLogFile();
if (entries.length === 0) {
return {
content: [{
type: 'text',
text: 'No terminal history found. Make sure to use the `cap` command to capture terminal output.\n\nExample: cap npm run dev',
}],
};
}
const last = entries[entries.length - 1];
const exitStatus = last.exitCode === 0 ? '✓ Success' : `✗ Failed (exit code: ${last.exitCode})`;
return {
content: [{
type: 'text',
text: `Command: ${last.command}\nStatus: ${exitStatus}\n\nOutput:\n${last.output}`,
}],
};
}
);
// Tool: Get the last error (only if last command failed)
server.registerTool(
'get_last_error',
{
title: 'Get Last Error',
description: 'Get the last terminal command only if it failed (non-zero exit code). Returns nothing if the last command succeeded.',
inputSchema: {},
},
async () => {
const entries = parseLogFile();
if (entries.length === 0) {
return {
content: [{
type: 'text',
text: 'No terminal history found.',
}],
};
}
const last = entries[entries.length - 1];
if (last.exitCode === 0 || last.exitCode === null) {
return {
content: [{
type: 'text',
text: 'Last command succeeded - no error to report.',
}],
};
}
return {
content: [{
type: 'text',
text: `Command: ${last.command}\nExit Code: ${last.exitCode}\n\nError Output:\n${last.output}`,
}],
};
}
);
// Tool: Get recent commands
server.registerTool(
'get_recent_commands',
{
title: 'Get Recent Commands',
description: 'Get the last N terminal commands and their outputs.',
inputSchema: {
count: z.number().min(1).max(20).default(5).describe('Number of recent commands to retrieve (1-20)'),
},
},
async ({ count = 5 }) => {
const entries = parseLogFile();
if (entries.length === 0) {
return {
content: [{
type: 'text',
text: 'No terminal history found.',
}],
};
}
const recent = entries.slice(-count);
const formatted = recent.map((entry, i) => {
const exitStatus = entry.exitCode === 0 ? '✓' : `✗ (${entry.exitCode})`;
return `[${i + 1}] ${exitStatus} $ ${entry.command}\n${entry.output}`;
}).join('\n\n---\n\n');
return {
content: [{
type: 'text',
text: `Last ${recent.length} commands:\n\n${formatted}`,
}],
};
}
);
// Tool: Search output for a pattern
server.registerTool(
'search_output',
{
title: 'Search Terminal Output',
description: 'Search through recent terminal output for a specific pattern or error message.',
inputSchema: {
pattern: z.string().describe('Text pattern to search for (case-insensitive)'),
count: z.number().min(1).max(50).default(10).describe('Number of recent commands to search through'),
},
},
async ({ pattern, count = 10 }) => {
const entries = parseLogFile();
if (entries.length === 0) {
return {
content: [{
type: 'text',
text: 'No terminal history found.',
}],
};
}
const recent = entries.slice(-count);
const regex = new RegExp(pattern, 'gi');
const matches = [];
for (const entry of recent) {
const fullText = `${entry.command}\n${entry.output}`;
if (regex.test(fullText)) {
// Find matching lines
const lines = fullText.split('\n');
const matchingLines = lines.filter(line => new RegExp(pattern, 'i').test(line));
matches.push({
command: entry.command,
exitCode: entry.exitCode,
matchingLines,
});
}
}
if (matches.length === 0) {
return {
content: [{
type: 'text',
text: `No matches found for "${pattern}" in the last ${count} commands.`,
}],
};
}
const formatted = matches.map(m => {
const status = m.exitCode === 0 ? '✓' : `✗ (${m.exitCode})`;
return `${status} $ ${m.command}\nMatching lines:\n${m.matchingLines.map(l => ` > ${l}`).join('\n')}`;
}).join('\n\n');
return {
content: [{
type: 'text',
text: `Found ${matches.length} command(s) matching "${pattern}":\n\n${formatted}`,
}],
};
}
);
// Tool: Clear the log file
server.registerTool(
'clear_history',
{
title: 'Clear Terminal History',
description: 'Clear the captured terminal history log file.',
inputSchema: {},
},
async () => {
try {
writeFileSync(LOG_FILE, '');
return {
content: [{
type: 'text',
text: 'Terminal history cleared.',
}],
};
} catch (error) {
return {
content: [{
type: 'text',
text: `Failed to clear history: ${error.message}`,
}],
};
}
}
);
// Connect to stdio transport for Claude Desktop
const transport = new StdioServerTransport();
await server.connect(transport);