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.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +29 -0
- package/dist/keys.js +158 -0
- package/dist/keys.js.map +1 -0
- package/dist/manager.d.ts +45 -0
- package/dist/manager.js +119 -0
- package/dist/manager.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +348 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +89 -0
- package/dist/session.js +231 -0
- package/dist/session.js.map +1 -0
- package/dist/sshConfig.d.ts +23 -0
- package/dist/sshConfig.js +95 -0
- package/dist/sshConfig.js.map +1 -0
- package/package.json +50 -0
package/dist/session.js
ADDED
|
@@ -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
|
+
}
|