-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathmcp-server.ts
More file actions
193 lines (178 loc) · 6.4 KB
/
Copy pathmcp-server.ts
File metadata and controls
193 lines (178 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import type { Args } from "./messages.js";
import { TerminalClient } from "./terminal-client.js";
export async function runMCPServer(): Promise<void> {
const serverClient = new TerminalClient();
const server = new Server(
{
name: "terminalcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
// Define the terminal tool
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "terminalcp",
description: `Control background processes with virtual terminals. IMPORTANT: Always clean up processes with "stop" action when done.
Examples:
Start dev server: {"action": "start", "command": "npm run dev", "cwd": "/path/to/project"}
Send text with Enter: {"action": "stdin", "id": "proc-123", "data": "npm test\\r"}
Send arrow keys: {"action": "stdin", "id": "proc-123", "data": "echo hello\\u001b[D\\u001b[D\\u001b[Dhi \\r"}
Send Ctrl+C: {"action": "stdin", "id": "proc-123", "data": "\\u0003"}
Stop process: {"action": "stop", "id": "proc-abc123"}
Stop all processes: {"action": "stop"}
Get terminal screen: {"action": "stdout", "id": "proc-123"} # Current view + scrollback
Get last 50 lines: {"action": "stdout", "id": "proc-123", "lines": 50}
Get all output ever: {"action": "stream", "id": "proc-123"} # From process start
Get new output only: {"action": "stream", "id": "proc-123", "since_last": true} # Since last stream call
Output modes:
stdout: Terminal emulator output - returns the rendered screen as user would see it.
Limited to 10K lines scrollback. Best for: interactive tools, TUIs, REPLs, debuggers.
stream: Raw process output - returns all text the process has written to stdout/stderr.
Strips ANSI codes by default (set strip_ansi: false to keep). No limit on history.
With since_last: true, returns only new output since last stream call on this process.
Best for: build logs, test output, monitoring long-running processes.
Common escape sequences for stdin:
Enter: \\r or \\u000d
Tab: \\t or \\u0009
Escape: \\u001b
Backspace: \\u007f
Ctrl+C: \\u0003
Ctrl+D: \\u0004
Ctrl+Z: \\u001a
Arrow keys: Up=\\u001b[A Down=\\u001b[B Right=\\u001b[C Left=\\u001b[D
Navigation: Home=\\u001b[H End=\\u001b[F PageUp=\\u001b[5~ PageDown=\\u001b[6~
Delete: \\u001b[3~ Insert: \\u001b[2~
Function keys: F1=\\u001bOP F2=\\u001bOQ F3=\\u001bOR F4=\\u001bOS
Meta/Alt: Alt+x=\\u001bx (ESC followed by character)
Interactive examples:
Vim: stdin "vim test.txt\\r" → stdin "iHello\\u001b:wq\\r" → stdout
Python: start "python3 -i" → stdin "2+2\\r" → stdout
Build monitoring: start "npm run build" → stream (since_last: true) → repeat
Interrupt: stdin "sleep 10\\r" → stdin "\\u0003" (Ctrl+C)
Note: Commands run via bash -c. Use absolute paths, not aliases.`,
inputSchema: {
type: "object",
properties: {
args: {
type: "object",
properties: {
action: {
type: "string",
enum: ["start", "stop", "stdout", "stdin", "list", "stream", "term-size", "kill-server"],
description: "The action to perform",
},
command: {
type: "string",
description: "Command to execute (required for 'start' action)",
},
cwd: {
type: "string",
description: "Working directory for the process (optional for 'start' action)",
},
name: {
type: "string",
description: "Human-readable name for the session (optional for 'start' action)",
},
id: {
type: "string",
description:
"Process ID (required for 'stdout', 'stdin', 'stream', 'term-size' actions; optional for 'stop' - if omitted, stops all processes)",
},
data: {
type: "string",
description:
"String to send to stdin with escape sequences (e.g., '\\r' for Enter, '\\u001b[A' for Up arrow)",
},
lines: {
type: "number",
description: "Number of lines to retrieve (optional for 'stdout' action)",
},
since_last: {
type: "boolean",
description:
"Only return output since last stream read (optional for 'stream' action, defaults to false)",
},
strip_ansi: {
type: "boolean",
description:
"Strip ANSI escape codes from output (optional for 'stream' action, defaults to true)",
},
},
required: ["action"],
additionalProperties: false,
},
},
required: ["args"],
additionalProperties: false,
},
},
],
}));
// Handle terminal tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== "terminalcp") {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const args = request.params.arguments?.args as Args;
if (!args || typeof args !== "object") {
throw new Error("Invalid arguments: expected JSON object");
}
try {
const result = await serverClient.request(args);
return {
content: [
{
type: "text",
text: result || "",
},
],
};
} catch (error) {
// Handle cases where server is not running - but NOT for start actions (let auto-start work)
if (error instanceof Error && (error.message === "No server running" || error.message === "Request timeout")) {
if (args.action === "list") {
return {
content: [
{
type: "text",
text: "No active sessions",
},
],
};
} else if (args.action === "start") {
// Don't catch errors for start actions - let the terminal client auto-start
throw error;
} else {
return {
content: [
{
type: "text",
text: `Error: No terminal server running. Use "list" to check status or start a process to auto-start the server.`,
},
],
};
}
}
throw error;
}
});
// Cleanup on exit
process.on("SIGINT", async () => {
console.error("Shutting down MCP server...");
serverClient.close();
process.exit(0);
});
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("terminalcp MCP server running on stdio");
}