index.ts•12.8 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { TerminalManager } from './terminal-manager-simple.js';
class TerminalMCPServer {
private server: Server;
private terminalManager: TerminalManager;
constructor() {
this.server = new Server(
{
name: 'node-terminal-mcp',
version: '1.0.1',
},
{
capabilities: {
tools: {},
},
}
);
this.terminalManager = new TerminalManager();
this.setupHandlers();
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'terminal_create',
description: 'Create a new persistent terminal session that will continue running until explicitly closed. Terminal sessions maintain their state, environment variables, working directory, and command history. Use this to establish a long-running interactive environment for executing commands, running processes, or maintaining context across multiple operations.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Unique identifier for the terminal session. Use descriptive names like "main", "build", "test", or "deploy" to organize different terminal contexts.',
},
shell: {
type: 'string',
description: 'Shell to use (default: process.env.SHELL or /bin/bash). Common options: /bin/bash, /bin/zsh, /bin/sh, cmd.exe (Windows), powershell.exe (Windows)',
default: process.env.SHELL || '/bin/bash',
},
cols: {
type: 'number',
description: 'Number of columns for terminal width (default: 80). Adjust based on expected output format.',
default: 80,
},
rows: {
type: 'number',
description: 'Number of rows for terminal height (default: 24). Adjust based on expected output length.',
default: 24,
},
},
required: ['sessionId'],
},
},
{
name: 'terminal_write',
description: 'Send text input to a terminal session. This is used for typing commands, text, or any character input. The terminal will process this input as if typed by a user. Always follow up with read_from_terminal to see the results and any output generated by the command.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Terminal session ID to send input to',
},
input: {
type: 'string',
description: 'Text input to send to the terminal. Can be commands, text, or any character sequence. Examples: "ls -la", "cd /home", "npm install", "echo hello"',
},
},
required: ['sessionId', 'input'],
},
},
{
name: 'terminal_send_key',
description: 'Send special keys and key combinations to a terminal session. Use this for navigation, control sequences, and interactive commands that require specific key presses. Essential for terminal interactions like confirming prompts, navigating command history, or sending interrupt signals.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Terminal session ID to send the key to',
},
key: {
type: 'string',
description: 'Special key to send. Supported keys: enter, tab, backspace, up, down, left, right, home, end, pageup, pagedown, delete, escape, ctrl+c, ctrl+d, ctrl+z, ctrl+l, ctrl+a, ctrl+e, ctrl+k, ctrl+u, ctrl+w, ctrl+r, ctrl+s, ctrl+q, space, f1-f12. Use "enter" to execute commands, "ctrl+c" to interrupt processes, "up/down" for command history.',
},
},
required: ['sessionId', 'key'],
},
},
{
name: 'terminal_read',
description: 'Read the current output buffer from a terminal session. CRITICAL: Always call this after sending input to see command results, error messages, prompts, or any terminal output. The terminal session maintains its state, so you should read frequently to stay updated on the current state, especially after commands that might change the working directory, environment, or running processes.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Terminal session ID to read output from',
},
},
required: ['sessionId'],
},
},
{
name: 'terminal_resize',
description: 'Resize a terminal session to adjust its dimensions. Useful when output is truncated or when you need to accommodate different content formats. Some commands and applications behave differently based on terminal size.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Terminal session ID to resize',
},
cols: {
type: 'number',
description: 'New number of columns (width). Common values: 80, 120, 160. Use larger values for wide output or tables.',
},
rows: {
type: 'number',
description: 'New number of rows (height). Common values: 24, 40, 60. Use larger values for long output or logs.',
},
},
required: ['sessionId', 'cols', 'rows'],
},
},
{
name: 'terminal_list',
description: 'List all currently active terminal sessions. Use this to check which terminal sessions are available before interacting with them, or to manage multiple terminal contexts. Each session maintains its own state, working directory, and environment.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'terminal_close',
description: 'Close a terminal session permanently. This will terminate the shell process and free up resources. Use this when you no longer need a terminal session or want to clean up. Note: Any running processes in the terminal will be terminated.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Terminal session ID to close',
},
},
required: ['sessionId'],
},
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'terminal_create':
return await this.handleCreateTerminal(args);
case 'terminal_write':
return await this.handleWriteToTerminal(args);
case 'terminal_send_key':
return await this.handleSendKeyToTerminal(args);
case 'terminal_read':
return await this.handleReadFromTerminal(args);
case 'terminal_resize':
return await this.handleResizeTerminal(args);
case 'terminal_list':
return await this.handleListTerminals(args);
case 'terminal_close':
return await this.handleCloseTerminal(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
}
private async handleCreateTerminal(args: any) {
const { sessionId, shell, cols, rows } = args;
await this.terminalManager.createSession(sessionId, { shell, cols, rows });
return {
content: [
{
type: 'text',
text: `Terminal session '${sessionId}' created successfully`,
},
],
};
}
private async handleWriteToTerminal(args: any) {
const { sessionId, input } = args;
await this.terminalManager.writeToSession(sessionId, input);
return {
content: [
{
type: 'text',
text: `Input sent to terminal '${sessionId}'`,
},
],
};
}
private async handleSendKeyToTerminal(args: any) {
const { sessionId, key } = args;
await this.terminalManager.sendKeyToSession(sessionId, key);
return {
content: [
{
type: 'text',
text: `Key '${key}' sent to terminal '${sessionId}'`,
},
],
};
}
private async handleReadFromTerminal(args: any) {
const { sessionId } = args;
const output = await this.terminalManager.readFromSession(sessionId);
return {
content: [
{
type: 'text',
text: output,
},
],
};
}
private async handleResizeTerminal(args: any) {
const { sessionId, cols, rows } = args;
await this.terminalManager.resizeSession(sessionId, cols, rows);
return {
content: [
{
type: 'text',
text: `Terminal '${sessionId}' resized to ${cols}x${rows}`,
},
],
};
}
private async handleListTerminals(args: any) {
const sessions = this.terminalManager.listSessions();
return {
content: [
{
type: 'text',
text: `Active terminal sessions: ${sessions.join(', ') || 'None'}`,
},
],
};
}
private async handleCloseTerminal(args: any) {
const { sessionId } = args;
await this.terminalManager.closeSession(sessionId);
return {
content: [
{
type: 'text',
text: `Terminal session '${sessionId}' closed`,
},
],
};
}
async start() {
try {
const transport = new StdioServerTransport();
// Add error handling for transport
transport.onerror = (error) => {
process.stderr.write(`Transport error: ${error}\n`);
};
transport.onclose = () => {
process.stderr.write(`Transport closed\n`);
process.exit(0);
};
await this.server.connect(transport);
process.stderr.write(`Server connected to transport\n`);
// Ensure stdin is open to prevent process exit in npm sandbox
process.stdin.resume();
// Handle process signals for graceful shutdown
process.on('SIGINT', async () => {
await this.terminalManager.closeAllSessions();
process.exit(0);
});
process.on('SIGTERM', async () => {
await this.terminalManager.closeAllSessions();
process.exit(0);
});
// Keep the process alive indefinitely
await new Promise(() => {});
} catch (error) {
process.stderr.write(`Failed to start MCP server: ${error}\n`);
process.exit(1);
}
}
}
// Start the server
const server = new TerminalMCPServer();
// Add process error handling
process.on('uncaughtException', (error) => {
process.stderr.write(`Uncaught exception: ${error}\n`);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
process.stderr.write(`Unhandled rejection: ${reason}\n`);
process.exit(1);
});
// Start the server and keep it running
(async () => {
try {
process.stderr.write('Starting MCP server...\n');
await server.start();
process.stderr.write('MCP server started successfully\n');
} catch (error) {
process.stderr.write(`Failed to start server: ${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
}
})();