mcp-ssh-terminal 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,231 @@
1
+ import { spawn } from "node-pty";
2
+ // @xterm/headless is a CJS bundle whose named exports are not statically
3
+ // detectable by Node's ESM loader, so import the default and destructure.
4
+ import xtermPkg from "@xterm/headless";
5
+ const { Terminal } = xtermPkg;
6
+ // Measured in UTF-16 code units (== bytes for ASCII terminal traffic).
7
+ const MAX_RAW_BYTES = 512 * 1024;
8
+ /**
9
+ * One persistent child process (typically the ssh client) attached to a PTY,
10
+ * with its output fed into a headless xterm emulator so callers can read a
11
+ * rendered screen instead of a raw escape-code stream.
12
+ */
13
+ export class Session {
14
+ id;
15
+ label;
16
+ startedAt = Date.now();
17
+ pty;
18
+ term;
19
+ raw = "";
20
+ lastDataAt = Date.now();
21
+ /** Bumped only by child output, never by our own writes. */
22
+ dataEvents = 0;
23
+ _exited = false;
24
+ _exitCode = null;
25
+ _exitSignal = null;
26
+ constructor(id, opts) {
27
+ this.id = id;
28
+ this.label = opts.label;
29
+ this.term = new Terminal({
30
+ cols: opts.cols,
31
+ rows: opts.rows,
32
+ scrollback: opts.scrollback ?? 5000,
33
+ allowProposedApi: true,
34
+ });
35
+ this.pty = spawn(opts.file, opts.args, {
36
+ name: "xterm-256color",
37
+ cols: opts.cols,
38
+ rows: opts.rows,
39
+ cwd: process.cwd(),
40
+ env: { ...process.env, TERM: "xterm-256color" },
41
+ });
42
+ this.pty.onData((data) => {
43
+ this.lastDataAt = Date.now();
44
+ this.dataEvents++;
45
+ this.term.write(data);
46
+ this.raw += data;
47
+ if (this.raw.length > MAX_RAW_BYTES) {
48
+ this.raw = this.raw.slice(this.raw.length - MAX_RAW_BYTES);
49
+ }
50
+ });
51
+ // The emulator generates replies to terminal queries from the remote side
52
+ // (cursor-position reports, device attributes, ...). They must be written
53
+ // back to the child, or programs that probe the terminal hang waiting.
54
+ // This fires from xterm's async parse loop — outside any tool-call
55
+ // try/catch — so a write to a just-died PTY must not crash the server.
56
+ this.term.onData((data) => {
57
+ if (this._exited)
58
+ return;
59
+ try {
60
+ this.pty.write(data);
61
+ }
62
+ catch {
63
+ /* pty torn down between the exit and onExit firing */
64
+ }
65
+ });
66
+ this.pty.onExit(({ exitCode, signal }) => {
67
+ this._exited = true;
68
+ this._exitCode = exitCode;
69
+ this._exitSignal = signal ?? null;
70
+ this.lastDataAt = Date.now();
71
+ });
72
+ }
73
+ get exited() {
74
+ return this._exited;
75
+ }
76
+ get exitCode() {
77
+ return this._exitCode;
78
+ }
79
+ get exitSignal() {
80
+ return this._exitSignal;
81
+ }
82
+ get pid() {
83
+ return this.pty.pid;
84
+ }
85
+ get dimensions() {
86
+ return { cols: this.term.cols, rows: this.term.rows };
87
+ }
88
+ /** True while the remote app has DECCKM (application cursor keys) enabled. */
89
+ get applicationCursorKeys() {
90
+ return this.term.modes.applicationCursorKeysMode;
91
+ }
92
+ /** Write raw bytes to the PTY. Marks activity so idle-waits behave. */
93
+ write(data) {
94
+ if (this._exited)
95
+ throw new Error(`Session ${this.id} has exited`);
96
+ this.lastDataAt = Date.now();
97
+ this.pty.write(data);
98
+ }
99
+ resize(cols, rows) {
100
+ if (this._exited)
101
+ throw new Error(`Session ${this.id} has exited`);
102
+ this.pty.resize(cols, rows);
103
+ this.term.resize(cols, rows);
104
+ }
105
+ /**
106
+ * Wait until the output has been quiet for `settleMs`, or `timeoutMs`
107
+ * elapses, or the process exits — whichever comes first. This is how we
108
+ * decide a command's output has "settled" enough to read.
109
+ *
110
+ * With `requireData`, the settle clock only counts once the child has
111
+ * produced at least one byte of output — so a slow connection (DNS, TCP,
112
+ * key exchange) isn't mistaken for an already-settled screen.
113
+ */
114
+ waitForIdle(settleMs, timeoutMs, requireData = false) {
115
+ const deadline = Date.now() + timeoutMs;
116
+ return new Promise((resolve) => {
117
+ const tick = () => {
118
+ if (this._exited)
119
+ return resolve({ reason: "exited" });
120
+ const now = Date.now();
121
+ if (requireData && this.dataEvents === 0) {
122
+ if (now >= deadline)
123
+ return resolve({ reason: "timeout" });
124
+ return void setTimeout(tick, Math.max(10, Math.min(50, deadline - now)));
125
+ }
126
+ const quietFor = now - this.lastDataAt;
127
+ if (quietFor >= settleMs)
128
+ return resolve({ reason: "idle" });
129
+ if (now >= deadline)
130
+ return resolve({ reason: "timeout" });
131
+ const nextQuiet = this.lastDataAt + settleMs - now;
132
+ const nextDeadline = deadline - now;
133
+ setTimeout(tick, Math.max(10, Math.min(nextQuiet, nextDeadline)));
134
+ };
135
+ setTimeout(tick, Math.max(10, Math.min(settleMs, timeoutMs)));
136
+ });
137
+ }
138
+ /**
139
+ * Wait for *new* child output (any byte after this call), the process
140
+ * exiting, or `timeoutMs` — whichever comes first. Unlike waitForIdle, an
141
+ * already-quiet session blocks here until something actually arrives.
142
+ */
143
+ waitForData(timeoutMs) {
144
+ const baseline = this.dataEvents;
145
+ const deadline = Date.now() + timeoutMs;
146
+ return new Promise((resolve) => {
147
+ const tick = () => {
148
+ if (this.dataEvents > baseline)
149
+ return resolve({ reason: "data" });
150
+ if (this._exited)
151
+ return resolve({ reason: "exited" });
152
+ const now = Date.now();
153
+ if (now >= deadline)
154
+ return resolve({ reason: "timeout" });
155
+ setTimeout(tick, Math.max(10, Math.min(25, deadline - now)));
156
+ };
157
+ tick();
158
+ });
159
+ }
160
+ /** Flush the xterm write queue so a subsequent render sees all output. */
161
+ flush() {
162
+ return new Promise((resolve) => this.term.write("", () => resolve()));
163
+ }
164
+ /**
165
+ * Render the emulated screen as text. Returns the tail of the buffer down
166
+ * to the bottom of the viewport (not just the cursor line — full-screen
167
+ * apps often leave the cursor mid-screen with content below it), capped at
168
+ * `maxLines`. Trailing blank lines are trimmed. This collapses
169
+ * redraws/colors/cursor moves into the plain text a human would see.
170
+ */
171
+ async render(maxLines) {
172
+ await this.flush();
173
+ const buf = this.term.buffer.active;
174
+ let lastRow = Math.min(buf.baseY + this.term.rows - 1, buf.length - 1);
175
+ const absCursorRow = buf.baseY + buf.cursorY;
176
+ // Skip trailing blank viewport rows (typical after a full-screen app
177
+ // exits) so a small maxLines window isn't spent on empty space — but
178
+ // never trim above the cursor line.
179
+ const isBlank = (i) => {
180
+ const line = buf.getLine(i);
181
+ return !line || line.translateToString(true) === "";
182
+ };
183
+ while (lastRow > absCursorRow && isBlank(lastRow))
184
+ lastRow--;
185
+ const start = Math.max(0, lastRow - maxLines + 1);
186
+ const lines = [];
187
+ for (let i = start; i <= lastRow; i++) {
188
+ const line = buf.getLine(i);
189
+ lines.push(line ? line.translateToString(true) : "");
190
+ }
191
+ while (lines.length > 0 && lines[lines.length - 1] === "")
192
+ lines.pop();
193
+ // Cursor row relative to the returned text block (buf.cursorY is
194
+ // viewport-relative and would drift when scrollback lines are shown).
195
+ return {
196
+ text: lines.join("\n"),
197
+ cursorRow: Math.max(0, absCursorRow - start),
198
+ cursorCol: buf.cursorX,
199
+ rows: this.term.rows,
200
+ cols: this.term.cols,
201
+ totalLines: buf.length,
202
+ shownLines: lines.length,
203
+ };
204
+ }
205
+ /** Return the tail of the raw PTY byte stream (escape codes included). */
206
+ rawTail(maxBytes) {
207
+ if (maxBytes >= this.raw.length)
208
+ return this.raw;
209
+ return this.raw.slice(this.raw.length - maxBytes);
210
+ }
211
+ /** Terminate the child. SIGHUP first; caller may escalate. */
212
+ close(signal = "SIGHUP") {
213
+ if (this._exited)
214
+ return;
215
+ try {
216
+ this.pty.kill(signal);
217
+ }
218
+ catch {
219
+ /* already gone */
220
+ }
221
+ }
222
+ kill() {
223
+ try {
224
+ this.pty.kill("SIGKILL");
225
+ }
226
+ catch {
227
+ /* ignore */
228
+ }
229
+ }
230
+ }
231
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /** Expand a leading ~ to the user's home directory. */
2
+ export declare function expandHome(p: string): string;
3
+ export interface HostEntry {
4
+ /** The Host pattern(s) on the entry line, e.g. "prod-web" or "*.internal". */
5
+ patterns: string[];
6
+ /** Resolved HostName if the entry sets one. */
7
+ hostName?: string;
8
+ /** Resolved User if the entry sets one. */
9
+ user?: string;
10
+ /** Resolved Port if the entry sets one. */
11
+ port?: string;
12
+ /** ProxyJump target if the entry sets one. */
13
+ proxyJump?: string;
14
+ /** File this entry was found in. */
15
+ source: string;
16
+ }
17
+ /**
18
+ * Best-effort parser for ~/.ssh/config, following `Include` directives. This
19
+ * is only for discoverability (listing candidate host aliases) — actual
20
+ * connection semantics are always delegated to the real ssh client, which is
21
+ * the authority on config resolution, Match blocks, tokens, etc.
22
+ */
23
+ export declare function listConfigHosts(configPath?: string): HostEntry[];
@@ -0,0 +1,95 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { globSync } from "node:fs";
5
+ /** Expand a leading ~ to the user's home directory. */
6
+ export function expandHome(p) {
7
+ if (p === "~")
8
+ return homedir();
9
+ if (p.startsWith("~/"))
10
+ return join(homedir(), p.slice(2));
11
+ return p;
12
+ }
13
+ /**
14
+ * Best-effort parser for ~/.ssh/config, following `Include` directives. This
15
+ * is only for discoverability (listing candidate host aliases) — actual
16
+ * connection semantics are always delegated to the real ssh client, which is
17
+ * the authority on config resolution, Match blocks, tokens, etc.
18
+ */
19
+ export function listConfigHosts(configPath) {
20
+ const start = configPath ? expandHome(configPath) : join(homedir(), ".ssh", "config");
21
+ const entries = [];
22
+ const seen = new Set();
23
+ const parseFile = (path) => {
24
+ const abs = resolve(path);
25
+ if (seen.has(abs))
26
+ return;
27
+ seen.add(abs);
28
+ if (!existsSync(abs))
29
+ return;
30
+ let content;
31
+ try {
32
+ content = readFileSync(abs, "utf8");
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ let current = null;
38
+ const flush = () => {
39
+ if (current)
40
+ entries.push(current);
41
+ current = null;
42
+ };
43
+ for (const rawLine of content.split(/\r?\n/)) {
44
+ const line = rawLine.trim();
45
+ if (line === "" || line.startsWith("#"))
46
+ continue;
47
+ const eq = line.replace(/^(\S+)\s*=\s*/, "$1 ");
48
+ const spaceIdx = eq.search(/\s/);
49
+ if (spaceIdx === -1)
50
+ continue;
51
+ const keyword = eq.slice(0, spaceIdx).toLowerCase();
52
+ const value = eq.slice(spaceIdx + 1).trim();
53
+ if (keyword === "host") {
54
+ flush();
55
+ current = { patterns: value.split(/\s+/), source: abs };
56
+ }
57
+ else if (keyword === "match") {
58
+ // A Match block ends the current Host entry. We don't model Match
59
+ // conditions (ssh itself is the authority) — but without this, the
60
+ // block's directives would be misattributed to the previous Host.
61
+ flush();
62
+ }
63
+ else if (keyword === "include") {
64
+ // Includes may reference multiple space-separated glob patterns.
65
+ for (const token of value.split(/\s+/)) {
66
+ const target = expandHome(token);
67
+ const globbed = isAbsolute(target) ? target : join(dirname(abs), target);
68
+ try {
69
+ for (const f of globSync(globbed))
70
+ parseFile(f);
71
+ }
72
+ catch {
73
+ /* ignore unmatched includes */
74
+ }
75
+ }
76
+ }
77
+ else if (current) {
78
+ if (keyword === "hostname")
79
+ current.hostName = value;
80
+ else if (keyword === "user")
81
+ current.user = value;
82
+ else if (keyword === "port")
83
+ current.port = value;
84
+ else if (keyword === "proxyjump")
85
+ current.proxyJump = value;
86
+ }
87
+ }
88
+ flush();
89
+ };
90
+ parseFile(start);
91
+ // Exclude pure-wildcard catch-all entries from the listing — they're not
92
+ // connectable destinations, just default blocks.
93
+ return entries.filter((e) => e.patterns.some((p) => p !== "*" && !p.startsWith("!")));
94
+ }
95
+ //# sourceMappingURL=sshConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sshConfig.js","sourceRoot":"","sources":["../src/sshConfig.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "mcp-ssh-terminal",
3
+ "version": "0.2.0",
4
+ "description": "MCP server exposing persistent, interactive SSH sessions driven through the real OpenSSH client and a headless terminal emulator.",
5
+ "bin": {
6
+ "mcp-ssh-terminal": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "start": "node dist/index.js",
14
+ "dev": "tsc -p tsconfig.json && node dist/index.js",
15
+ "test": "vitest run",
16
+ "prepublishOnly": "npm run build && npm test"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "ssh",
21
+ "terminal",
22
+ "routeros",
23
+ "mikrotik"
24
+ ],
25
+ "author": "Kirill Zhdanov",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/zhdkirill/mcp-ssh-terminal.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/zhdkirill/mcp-ssh-terminal/issues"
33
+ },
34
+ "homepage": "https://github.com/zhdkirill/mcp-ssh-terminal#readme",
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.29.0",
41
+ "@xterm/headless": "^6.0.0",
42
+ "node-pty": "^1.1.0",
43
+ "zod": "^4.4.3"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^26.1.1",
47
+ "typescript": "^7.0.2",
48
+ "vitest": "^4.1.10"
49
+ }
50
+ }