enterprise-ssh-mcp-server 0.3.0 → 0.3.2
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/CHANGELOG.md +11 -0
- package/README.md +97 -0
- package/dist/app/create-app.d.ts +4 -0
- package/dist/app/create-app.js +38 -0
- package/dist/app/create-app.js.map +1 -0
- package/dist/app/tool-context.d.ts +258 -0
- package/dist/app/tool-context.js +683 -0
- package/dist/app/tool-context.js.map +1 -0
- package/dist/env-profiles.d.ts +1 -0
- package/dist/env-profiles.js +6 -2
- package/dist/env-profiles.js.map +1 -1
- package/dist/env-reload.d.ts +36 -0
- package/dist/env-reload.js +65 -0
- package/dist/env-reload.js.map +1 -0
- package/dist/host-ca-trust-config.d.ts +10 -0
- package/dist/host-ca-trust-config.js +43 -0
- package/dist/host-ca-trust-config.js.map +1 -0
- package/dist/index.js +5 -1798
- package/dist/index.js.map +1 -1
- package/dist/openssh-command.d.ts +32 -0
- package/dist/openssh-command.js +207 -0
- package/dist/openssh-command.js.map +1 -0
- package/dist/openssh-temporary.d.ts +23 -0
- package/dist/openssh-temporary.js +81 -0
- package/dist/openssh-temporary.js.map +1 -0
- package/dist/openssh-trust.d.ts +15 -0
- package/dist/openssh-trust.js +91 -0
- package/dist/openssh-trust.js.map +1 -0
- package/dist/runtime-config.d.ts +1 -0
- package/dist/runtime-config.js +10 -1
- package/dist/runtime-config.js.map +1 -1
- package/dist/ssh-host-key.d.ts +27 -0
- package/dist/ssh-host-key.js +235 -0
- package/dist/ssh-host-key.js.map +1 -0
- package/dist/ssh-session.js +7 -3
- package/dist/ssh-session.js.map +1 -1
- package/dist/ssh-target.d.ts +19 -0
- package/dist/ssh-target.js +58 -0
- package/dist/ssh-target.js.map +1 -0
- package/dist/tools/register-config-profile-tools.d.ts +2 -0
- package/dist/tools/register-config-profile-tools.js +179 -0
- package/dist/tools/register-config-profile-tools.js.map +1 -0
- package/dist/tools/register-project-tools.d.ts +2 -0
- package/dist/tools/register-project-tools.js +377 -0
- package/dist/tools/register-project-tools.js.map +1 -0
- package/dist/tools/register-session-tools.d.ts +2 -0
- package/dist/tools/register-session-tools.js +293 -0
- package/dist/tools/register-session-tools.js.map +1 -0
- package/dist/tools/register-sftp-tools.d.ts +2 -0
- package/dist/tools/register-sftp-tools.js +172 -0
- package/dist/tools/register-sftp-tools.js.map +1 -0
- package/dist/tools/register-transfer-tools.d.ts +2 -0
- package/dist/tools/register-transfer-tools.js +167 -0
- package/dist/tools/register-transfer-tools.js.map +1 -0
- package/dist/tools/register-tunnel-tools.d.ts +2 -0
- package/dist/tools/register-tunnel-tools.js +55 -0
- package/dist/tools/register-tunnel-tools.js.map +1 -0
- package/dist/tools/tool-metadata.d.ts +18 -0
- package/dist/tools/tool-metadata.js +222 -0
- package/dist/tools/tool-metadata.js.map +1 -0
- package/package.json +5 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { McpToolError } from "./tool-result.js";
|
|
2
|
+
/** InstaBox SSH tokens are bearer credentials encoded as the SSH username. */
|
|
3
|
+
export function redactSshUsername(username) {
|
|
4
|
+
return /^ibssh_/i.test(username) ? "ibssh_[REDACTED]" : username;
|
|
5
|
+
}
|
|
6
|
+
export function isInstaboxSshTokenTarget(target) {
|
|
7
|
+
return target.host.toLowerCase() === "ssh.instabox.sh" && /^ibssh_/i.test(target.username);
|
|
8
|
+
}
|
|
9
|
+
/** Reject option-like hosts and characters that could alter SSH config/CLI parsing. */
|
|
10
|
+
export function validateSshHostname(host) {
|
|
11
|
+
if (host.length === 0
|
|
12
|
+
|| host.startsWith("-")
|
|
13
|
+
|| /\s/.test(host)
|
|
14
|
+
|| host.includes("@")
|
|
15
|
+
|| host.includes("/")
|
|
16
|
+
|| host.includes("\\")
|
|
17
|
+
|| host.includes("://")
|
|
18
|
+
|| !/^[A-Za-z0-9._:-]+$/.test(host)) {
|
|
19
|
+
throw new McpToolError("ssh_target_invalid", "SSH host must be a hostname/IP without option prefixes, URL schemes, paths, whitespace, or shell/config metacharacters", { field: "host" });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Accept either separate host/username fields or the common SSH target form
|
|
24
|
+
* "username@hostname". This parses the target only; it does not authenticate
|
|
25
|
+
* the caller or establish trust in the remote host key.
|
|
26
|
+
*/
|
|
27
|
+
export function parseSshTarget(input) {
|
|
28
|
+
const rawHost = input.host.trim();
|
|
29
|
+
const suppliedUsername = input.username?.trim();
|
|
30
|
+
if (rawHost.length === 0) {
|
|
31
|
+
throw new McpToolError("ssh_target_invalid", "SSH host must not be empty", { field: "host" });
|
|
32
|
+
}
|
|
33
|
+
if (/\s/.test(rawHost) || rawHost.includes("://") || rawHost.includes("/") || rawHost.includes("\\")) {
|
|
34
|
+
throw new McpToolError("ssh_target_invalid", "SSH host must be a hostname/IP, optionally prefixed with username@; do not include a URL scheme, path, or whitespace", { field: "host" });
|
|
35
|
+
}
|
|
36
|
+
const firstAt = rawHost.indexOf("@");
|
|
37
|
+
if (firstAt >= 0) {
|
|
38
|
+
if (firstAt === 0 || firstAt === rawHost.length - 1 || rawHost.indexOf("@", firstAt + 1) >= 0) {
|
|
39
|
+
throw new McpToolError("ssh_target_invalid", "Expected SSH target in username@hostname format", { field: "host" });
|
|
40
|
+
}
|
|
41
|
+
const embeddedUsername = rawHost.slice(0, firstAt);
|
|
42
|
+
const hostname = rawHost.slice(firstAt + 1);
|
|
43
|
+
validateSshHostname(hostname);
|
|
44
|
+
if (suppliedUsername !== undefined && suppliedUsername.length > 0 && suppliedUsername !== embeddedUsername) {
|
|
45
|
+
throw new McpToolError("ssh_target_username_conflict", "The username in host does not match the separately supplied username", { embeddedUsername, suppliedUsername });
|
|
46
|
+
}
|
|
47
|
+
return { host: hostname, username: embeddedUsername };
|
|
48
|
+
}
|
|
49
|
+
if (suppliedUsername === undefined || suppliedUsername.length === 0) {
|
|
50
|
+
throw new McpToolError("ssh_target_username_required", "Provide username separately, or set host to username@hostname", { field: "username" });
|
|
51
|
+
}
|
|
52
|
+
validateSshHostname(rawHost);
|
|
53
|
+
if (suppliedUsername.includes("@") || /\s/.test(suppliedUsername)) {
|
|
54
|
+
throw new McpToolError("ssh_target_invalid", "SSH username must not contain @ or whitespace", { field: "username" });
|
|
55
|
+
}
|
|
56
|
+
return { host: rawHost, username: suppliedUsername };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=ssh-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-target.js","sourceRoot":"","sources":["../src/ssh-target.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ToolContext } from "../app/tool-context.js";
|
|
2
|
+
export declare function registerConfigProfileTools(ctx: Pick<ToolContext, "z" | "toToolResult" | "profiles" | "runtimeConfig" | "reloadEnvState" | "sessions" | "wireEncodingSchema" | "remoteCheckNameSchema" | "DEFAULT_REMOTE_CHECKS" | "REMOTE_CHECK_COMMANDS" | "registerTool" | "connectProfileSession" | "buildRemoteCommand" | "runRemoteCheck">): void;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export function registerConfigProfileTools(ctx) {
|
|
2
|
+
const { z, toToolResult, profiles, runtimeConfig, reloadEnvState, sessions, wireEncodingSchema, remoteCheckNameSchema, DEFAULT_REMOTE_CHECKS, REMOTE_CHECK_COMMANDS, registerTool, connectProfileSession, buildRemoteCommand, runRemoteCheck } = ctx;
|
|
3
|
+
registerTool("ssh_get_config", {
|
|
4
|
+
title: "Get SSH MCP runtime config",
|
|
5
|
+
description: "Return non-secret runtime defaults loaded from the MCP .env file.",
|
|
6
|
+
inputSchema: {},
|
|
7
|
+
}, async () => toToolResult(async () => ({ ok: true, config: runtimeConfig.get() })));
|
|
8
|
+
registerTool("ssh_reload_config", {
|
|
9
|
+
title: "Reload SSH MCP runtime config",
|
|
10
|
+
description: "Atomically reload runtime defaults and SSH profiles from the MCP .env file without restarting. Tool visibility still reflects startup settings and may require an MCP restart.",
|
|
11
|
+
inputSchema: {},
|
|
12
|
+
}, async () => toToolResult(async () => {
|
|
13
|
+
const snapshot = reloadEnvState();
|
|
14
|
+
return {
|
|
15
|
+
ok: true,
|
|
16
|
+
config: snapshot.config,
|
|
17
|
+
profilesReloaded: snapshot.profiles.length,
|
|
18
|
+
restartRequiredForToolVisibility: snapshot.restartRequiredForToolVisibility,
|
|
19
|
+
note: "Runtime defaults and profiles were reloaded as one validated snapshot. If toolset or dangerous-tool visibility settings differ from startup, restart the MCP server to apply them.",
|
|
20
|
+
};
|
|
21
|
+
}));
|
|
22
|
+
registerTool("ssh_list_profiles", {
|
|
23
|
+
title: "List SSH profiles",
|
|
24
|
+
description: "List SSH server profiles loaded from the MCP server .env file. Secret values are never returned.",
|
|
25
|
+
inputSchema: {},
|
|
26
|
+
}, async () => toToolResult(async () => ({
|
|
27
|
+
ok: true,
|
|
28
|
+
envPath: profiles.sourcePath(),
|
|
29
|
+
profiles: profiles.list(),
|
|
30
|
+
})));
|
|
31
|
+
registerTool("ssh_reload_profiles", {
|
|
32
|
+
title: "Reload SSH profiles",
|
|
33
|
+
description: "Atomically reload SSH profiles and runtime defaults from the MCP server .env file without restarting the MCP server.",
|
|
34
|
+
inputSchema: {},
|
|
35
|
+
}, async () => toToolResult(async () => {
|
|
36
|
+
const snapshot = reloadEnvState();
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
envPath: profiles.sourcePath(),
|
|
40
|
+
profiles: snapshot.profiles,
|
|
41
|
+
runtimeConfigReloaded: true,
|
|
42
|
+
restartRequiredForToolVisibility: snapshot.restartRequiredForToolVisibility,
|
|
43
|
+
};
|
|
44
|
+
}));
|
|
45
|
+
registerTool("ssh_connect_profile", {
|
|
46
|
+
title: "Open SSH profile session",
|
|
47
|
+
description: "Open a persistent SSH session using an SSH_MCP_SERVER_<N>_* profile from .env.",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
profileName: z.string().min(1),
|
|
50
|
+
sessionName: z.string().min(1).optional(),
|
|
51
|
+
tryKeyboard: z.boolean().optional(),
|
|
52
|
+
keyboardResponses: z.array(z.string()).default([]),
|
|
53
|
+
agentForward: z.boolean().optional(),
|
|
54
|
+
keepaliveIntervalMs: z.number().int().min(0).optional(),
|
|
55
|
+
keepaliveCountMax: z.number().int().min(1).optional(),
|
|
56
|
+
readyTimeoutMs: z.number().int().min(1_000).optional(),
|
|
57
|
+
connectionTimeoutMs: z.number().int().min(1_000).optional(),
|
|
58
|
+
hostHash: z.enum(["md5", "sha1", "sha256"]).optional(),
|
|
59
|
+
expectedHostHash: z.string().optional(),
|
|
60
|
+
strictVendor: z.boolean().default(true),
|
|
61
|
+
debug: z.boolean().default(false),
|
|
62
|
+
},
|
|
63
|
+
}, async (args) => toToolResult(async () => {
|
|
64
|
+
const connected = await connectProfileSession({
|
|
65
|
+
profileName: args.profileName,
|
|
66
|
+
sessionName: args.sessionName,
|
|
67
|
+
agentForward: args.agentForward,
|
|
68
|
+
tryKeyboard: args.tryKeyboard,
|
|
69
|
+
keyboardResponses: args.keyboardResponses,
|
|
70
|
+
keepaliveIntervalMs: args.keepaliveIntervalMs,
|
|
71
|
+
keepaliveCountMax: args.keepaliveCountMax,
|
|
72
|
+
readyTimeoutMs: args.readyTimeoutMs,
|
|
73
|
+
connectionTimeoutMs: args.connectionTimeoutMs,
|
|
74
|
+
strictVendor: args.strictVendor,
|
|
75
|
+
debug: args.debug,
|
|
76
|
+
hostHash: args.hostHash,
|
|
77
|
+
expectedHostHash: args.expectedHostHash,
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
profile: connected.profile,
|
|
82
|
+
session: connected.session,
|
|
83
|
+
};
|
|
84
|
+
}));
|
|
85
|
+
registerTool("ssh_run_profile", {
|
|
86
|
+
title: "Run command on SSH profile",
|
|
87
|
+
description: "Use this first for one-shot remote commands on a configured SSH profile. Prefer this over connect + exec when the user only asks to run a command or inspect a server.",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
profileName: z.string().min(1),
|
|
90
|
+
command: z.string().min(1),
|
|
91
|
+
workingDirectory: z.string().min(1).optional(),
|
|
92
|
+
keepSession: z.boolean().default(false),
|
|
93
|
+
outputEncoding: wireEncodingSchema,
|
|
94
|
+
timeoutMs: z.number().int().min(1).optional(),
|
|
95
|
+
maxOutputBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional(),
|
|
96
|
+
},
|
|
97
|
+
}, async (args) => toToolResult(async () => {
|
|
98
|
+
const config = runtimeConfig.get();
|
|
99
|
+
const connected = await connectProfileSession({
|
|
100
|
+
profileName: args.profileName,
|
|
101
|
+
keyboardResponses: [],
|
|
102
|
+
strictVendor: true,
|
|
103
|
+
debug: false,
|
|
104
|
+
});
|
|
105
|
+
const command = buildRemoteCommand(args.command, args.workingDirectory);
|
|
106
|
+
try {
|
|
107
|
+
const result = await sessions.get(connected.session.id).exec({
|
|
108
|
+
command,
|
|
109
|
+
stdinEncoding: "utf8",
|
|
110
|
+
outputEncoding: args.outputEncoding,
|
|
111
|
+
timeoutMs: args.timeoutMs ?? config.defaultExecTimeoutMs,
|
|
112
|
+
maxOutputBytes: args.maxOutputBytes ?? config.defaultMaxOutputBytes,
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
profile: connected.profile,
|
|
117
|
+
session: connected.session,
|
|
118
|
+
keptSession: args.keepSession,
|
|
119
|
+
result,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (!args.keepSession) {
|
|
124
|
+
await sessions.disconnect(connected.session.id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
registerTool("ssh_check_profile", {
|
|
129
|
+
title: "Check SSH profile host",
|
|
130
|
+
description: "Agent-friendly one-shot remote health check: connect to a named profile, run safe built-in inspection commands, return structured results, and disconnect.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
profileName: z.string().min(1),
|
|
133
|
+
checks: z.array(remoteCheckNameSchema).default(DEFAULT_REMOTE_CHECKS),
|
|
134
|
+
workingDirectory: z.string().min(1).optional(),
|
|
135
|
+
timeoutMs: z.number().int().min(1).optional(),
|
|
136
|
+
maxOutputBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional(),
|
|
137
|
+
},
|
|
138
|
+
}, async (args) => toToolResult(async () => {
|
|
139
|
+
const config = runtimeConfig.get();
|
|
140
|
+
const connected = await connectProfileSession({
|
|
141
|
+
profileName: args.profileName,
|
|
142
|
+
keyboardResponses: [],
|
|
143
|
+
strictVendor: true,
|
|
144
|
+
debug: false,
|
|
145
|
+
});
|
|
146
|
+
const session = sessions.get(connected.session.id);
|
|
147
|
+
const selectedChecks = [...new Set(args.checks)];
|
|
148
|
+
try {
|
|
149
|
+
const checks = [];
|
|
150
|
+
for (const checkName of selectedChecks) {
|
|
151
|
+
const check = REMOTE_CHECK_COMMANDS[checkName];
|
|
152
|
+
checks.push(await runRemoteCheck({
|
|
153
|
+
name: checkName,
|
|
154
|
+
title: check.title,
|
|
155
|
+
command: buildRemoteCommand(check.command, args.workingDirectory),
|
|
156
|
+
timeoutMs: args.timeoutMs ?? config.defaultExecTimeoutMs,
|
|
157
|
+
maxOutputBytes: args.maxOutputBytes ?? config.defaultMaxOutputBytes,
|
|
158
|
+
execute: (command) => session.exec({
|
|
159
|
+
command,
|
|
160
|
+
stdinEncoding: "utf8",
|
|
161
|
+
outputEncoding: "utf8",
|
|
162
|
+
timeoutMs: args.timeoutMs ?? config.defaultExecTimeoutMs,
|
|
163
|
+
maxOutputBytes: args.maxOutputBytes ?? config.defaultMaxOutputBytes,
|
|
164
|
+
}),
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
profile: connected.profile,
|
|
170
|
+
session: connected.session,
|
|
171
|
+
checks,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
await sessions.disconnect(connected.session.id);
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=register-config-profile-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-config-profile-tools.js","sourceRoot":"","sources":["../../src/tools/register-config-profile-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ToolContext } from "../app/tool-context.js";
|
|
2
|
+
export declare function registerProjectTools(ctx: Pick<ToolContext, "z" | "sftpStat" | "statsToJson" | "McpToolError" | "toToolResult" | "defaultTreeOptions" | "maxEditFileBytes" | "parseUnifiedPatch" | "publicSnapshot" | "readRemoteFileSnapshot" | "readRemoteTree" | "resolveSafeRemotePath" | "sha256" | "runtimeConfig" | "sessions" | "editHistory" | "projectRootSchema" | "fileEncodingSchema" | "expectedShaSchema" | "expectedFileSchema" | "projectPathSchema" | "registerTool" | "connectProjectWorkspace" | "inspectRemoteProject" | "projectProfileSummary" | "writeProjectFile" | "applySinglePatchFile" | "validateProjectPatch" | "applyProjectPatch" | "deleteProjectFile" | "moveProjectFile" | "rollbackEdit" | "resolveProjectPathArgs" | "assertPatchSize" | "compactTreeOptions" | "buildProjectSearchCommand" | "buildProjectDiffCommand" | "parseSearchMatches" | "normalizeProfileNameForHistory" | "buildRemoteCommand">): void;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
export function registerProjectTools(ctx) {
|
|
2
|
+
const { z, sftpStat, statsToJson, McpToolError, toToolResult, defaultTreeOptions, maxEditFileBytes, parseUnifiedPatch, publicSnapshot, readRemoteFileSnapshot, readRemoteTree, resolveSafeRemotePath, sha256, runtimeConfig, sessions, editHistory, projectRootSchema, fileEncodingSchema, expectedShaSchema, expectedFileSchema, projectPathSchema, registerTool, connectProjectWorkspace, inspectRemoteProject, projectProfileSummary, writeProjectFile, applySinglePatchFile, validateProjectPatch, applyProjectPatch, deleteProjectFile, moveProjectFile, rollbackEdit, resolveProjectPathArgs, assertPatchSize, compactTreeOptions, buildProjectSearchCommand, buildProjectDiffCommand, parseSearchMatches, normalizeProfileNameForHistory, buildRemoteCommand } = ctx;
|
|
3
|
+
registerTool("ssh_project_open", {
|
|
4
|
+
title: "Open SSH project",
|
|
5
|
+
description: "Resolve and inspect the editable project root for a configured SSH profile.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
profileName: z.string().min(1),
|
|
8
|
+
projectRoot: projectRootSchema,
|
|
9
|
+
},
|
|
10
|
+
}, async (args) => toToolResult(async () => {
|
|
11
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
12
|
+
try {
|
|
13
|
+
return {
|
|
14
|
+
ok: true,
|
|
15
|
+
profile: projectProfileSummary(workspace.profile),
|
|
16
|
+
project: await inspectRemoteProject(workspace.session, workspace.rootPath),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await sessions.disconnect(workspace.sessionId);
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
registerTool("ssh_project_tree", {
|
|
24
|
+
title: "Read SSH project tree",
|
|
25
|
+
description: "Return a bounded remote project tree rooted inside the profile edit root/default directory.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
profileName: z.string().min(1),
|
|
28
|
+
projectRoot: projectRootSchema,
|
|
29
|
+
maxDepth: z.number().int().min(1).max(12).optional(),
|
|
30
|
+
maxEntries: z.number().int().min(1).max(10_000).optional(),
|
|
31
|
+
includeHidden: z.boolean().optional(),
|
|
32
|
+
ignoreNames: z.array(z.string().min(1)).optional(),
|
|
33
|
+
},
|
|
34
|
+
}, async (args) => toToolResult(async () => {
|
|
35
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
36
|
+
try {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
tree: await readRemoteTree(workspace.sftp, workspace.rootPath, defaultTreeOptions(compactTreeOptions(args.maxDepth, args.maxEntries, args.includeHidden, args.ignoreNames))),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await sessions.disconnect(workspace.sessionId);
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
registerTool("ssh_project_search", {
|
|
47
|
+
title: "Search SSH project",
|
|
48
|
+
description: "Search text in a remote project using rg when available, with grep fallback.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
profileName: z.string().min(1),
|
|
51
|
+
query: z.string().min(1),
|
|
52
|
+
projectRoot: projectRootSchema,
|
|
53
|
+
paths: z.array(projectPathSchema).default([]),
|
|
54
|
+
literal: z.boolean().default(true),
|
|
55
|
+
maxResults: z.number().int().min(1).max(1000).default(100),
|
|
56
|
+
contextLines: z.number().int().min(0).max(5).default(0),
|
|
57
|
+
timeoutMs: z.number().int().min(1).optional(),
|
|
58
|
+
maxOutputBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional(),
|
|
59
|
+
},
|
|
60
|
+
}, async (args) => toToolResult(async () => {
|
|
61
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
62
|
+
try {
|
|
63
|
+
const config = runtimeConfig.get();
|
|
64
|
+
const paths = await resolveProjectPathArgs(workspace, args.paths, true);
|
|
65
|
+
const result = await workspace.session.exec({
|
|
66
|
+
command: buildProjectSearchCommand(workspace.rootPath, args.query, paths, args.literal, args.maxResults, args.contextLines),
|
|
67
|
+
stdinEncoding: "utf8",
|
|
68
|
+
outputEncoding: "utf8",
|
|
69
|
+
timeoutMs: args.timeoutMs ?? config.defaultExecTimeoutMs,
|
|
70
|
+
maxOutputBytes: args.maxOutputBytes ?? config.defaultMaxOutputBytes,
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
projectRoot: workspace.rootPath,
|
|
75
|
+
matches: parseSearchMatches(result.stdout.text ?? ""),
|
|
76
|
+
truncated: result.stdout.truncated,
|
|
77
|
+
result,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await sessions.disconnect(workspace.sessionId);
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
84
|
+
registerTool("ssh_project_stat", {
|
|
85
|
+
title: "Stat SSH project path",
|
|
86
|
+
description: "Return metadata for a file or directory inside the project root, with sha256 for editable files.",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
profileName: z.string().min(1),
|
|
89
|
+
path: projectPathSchema,
|
|
90
|
+
projectRoot: projectRootSchema,
|
|
91
|
+
},
|
|
92
|
+
}, async (args) => toToolResult(async () => {
|
|
93
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
94
|
+
try {
|
|
95
|
+
const safePath = await resolveSafeRemotePath(workspace.sftp, workspace.profile, workspace.rootPath, args.path, true);
|
|
96
|
+
const stats = await sftpStat(workspace.sftp, safePath.remotePath, true);
|
|
97
|
+
const payload = {
|
|
98
|
+
ok: true,
|
|
99
|
+
path: args.path,
|
|
100
|
+
remotePath: safePath.remotePath,
|
|
101
|
+
stats: statsToJson(stats),
|
|
102
|
+
};
|
|
103
|
+
if (stats.isFile() && stats.size <= maxEditFileBytes(workspace.profile, runtimeConfig.get().defaultMaxEditFileBytes)) {
|
|
104
|
+
payload.sha256 = (await readRemoteFileSnapshot(workspace.sftp, safePath.remotePath, stats.size)).sha256;
|
|
105
|
+
}
|
|
106
|
+
return payload;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await sessions.disconnect(workspace.sessionId);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
registerTool("ssh_file_read", {
|
|
113
|
+
title: "Read SSH project file",
|
|
114
|
+
description: "Read a bounded file inside the project root and return content plus sha256 for safe edits.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
profileName: z.string().min(1),
|
|
117
|
+
path: projectPathSchema,
|
|
118
|
+
projectRoot: projectRootSchema,
|
|
119
|
+
encoding: fileEncodingSchema,
|
|
120
|
+
maxBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional(),
|
|
121
|
+
},
|
|
122
|
+
}, async (args) => toToolResult(async () => {
|
|
123
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
124
|
+
try {
|
|
125
|
+
const safePath = await resolveSafeRemotePath(workspace.sftp, workspace.profile, workspace.rootPath, args.path, true);
|
|
126
|
+
const snapshot = await readRemoteFileSnapshot(workspace.sftp, safePath.remotePath, args.maxBytes ?? maxEditFileBytes(workspace.profile, runtimeConfig.get().defaultMaxEditFileBytes));
|
|
127
|
+
return { ok: true, file: publicSnapshot(snapshot, args.encoding) };
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await sessions.disconnect(workspace.sessionId);
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
registerTool("ssh_file_patch", {
|
|
134
|
+
title: "Patch SSH project file",
|
|
135
|
+
description: "Apply a unified diff to one file inside the project root using sha256 concurrency protection and atomic write.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
profileName: z.string().min(1),
|
|
138
|
+
path: projectPathSchema,
|
|
139
|
+
projectRoot: projectRootSchema,
|
|
140
|
+
unifiedPatch: z.string().min(1),
|
|
141
|
+
expectedSha256: expectedShaSchema,
|
|
142
|
+
createDirs: z.boolean().default(false),
|
|
143
|
+
},
|
|
144
|
+
annotations: { destructiveHint: true },
|
|
145
|
+
}, async (args) => toToolResult(async () => {
|
|
146
|
+
assertPatchSize(args.unifiedPatch);
|
|
147
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, true);
|
|
148
|
+
try {
|
|
149
|
+
const patchFiles = parseUnifiedPatch(args.unifiedPatch);
|
|
150
|
+
if (patchFiles.length !== 1) {
|
|
151
|
+
throw new McpToolError("ssh_file_patch_file_count_invalid", "ssh_file_patch expects exactly one file in unifiedPatch", {
|
|
152
|
+
fileCount: patchFiles.length,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
edit: await applySinglePatchFile(workspace, args.path, patchFiles[0], args.expectedSha256, args.createDirs),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
await sessions.disconnect(workspace.sessionId);
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
registerTool("ssh_project_validate_patch", {
|
|
165
|
+
title: "Validate SSH project patch",
|
|
166
|
+
description: "Validate that a multi-file unified diff applies cleanly without modifying remote files.",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
profileName: z.string().min(1),
|
|
169
|
+
projectRoot: projectRootSchema,
|
|
170
|
+
unifiedPatch: z.string().min(1),
|
|
171
|
+
expectedFiles: z.array(expectedFileSchema).default([]),
|
|
172
|
+
},
|
|
173
|
+
}, async (args) => toToolResult(async () => {
|
|
174
|
+
assertPatchSize(args.unifiedPatch);
|
|
175
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
176
|
+
try {
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
validation: await validateProjectPatch(workspace, parseUnifiedPatch(args.unifiedPatch), args.expectedFiles),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await sessions.disconnect(workspace.sessionId);
|
|
184
|
+
}
|
|
185
|
+
}));
|
|
186
|
+
registerTool("ssh_project_apply_patch", {
|
|
187
|
+
title: "Apply SSH project patch",
|
|
188
|
+
description: "Apply a multi-file unified diff inside the project root with backups, sha256 protection, and rollback on failure.",
|
|
189
|
+
inputSchema: {
|
|
190
|
+
profileName: z.string().min(1),
|
|
191
|
+
projectRoot: projectRootSchema,
|
|
192
|
+
unifiedPatch: z.string().min(1),
|
|
193
|
+
expectedFiles: z.array(expectedFileSchema).min(1),
|
|
194
|
+
createDirs: z.boolean().default(false),
|
|
195
|
+
},
|
|
196
|
+
annotations: { destructiveHint: true },
|
|
197
|
+
}, async (args) => toToolResult(async () => {
|
|
198
|
+
assertPatchSize(args.unifiedPatch);
|
|
199
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, true);
|
|
200
|
+
try {
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
edit: await applyProjectPatch(workspace, parseUnifiedPatch(args.unifiedPatch), args.expectedFiles, args.createDirs),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await sessions.disconnect(workspace.sessionId);
|
|
208
|
+
}
|
|
209
|
+
}));
|
|
210
|
+
registerTool("ssh_file_write", {
|
|
211
|
+
title: "Write SSH project file",
|
|
212
|
+
description: "Overwrite or create one project file with sha256/absent protection and atomic write.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
profileName: z.string().min(1),
|
|
215
|
+
path: projectPathSchema,
|
|
216
|
+
projectRoot: projectRootSchema,
|
|
217
|
+
data: z.string(),
|
|
218
|
+
encoding: fileEncodingSchema,
|
|
219
|
+
expectedSha256: expectedShaSchema.optional(),
|
|
220
|
+
createDirs: z.boolean().default(false),
|
|
221
|
+
mode: z.number().int().min(0).max(0o7777).optional(),
|
|
222
|
+
},
|
|
223
|
+
annotations: { destructiveHint: true },
|
|
224
|
+
}, async (args) => toToolResult(async () => {
|
|
225
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, true);
|
|
226
|
+
try {
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
edit: await writeProjectFile(workspace, args.path, args.encoding === "base64" ? Buffer.from(args.data, "base64") : Buffer.from(args.data, "utf8"), args.expectedSha256, args.createDirs, args.mode),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await sessions.disconnect(workspace.sessionId);
|
|
234
|
+
}
|
|
235
|
+
}));
|
|
236
|
+
registerTool("ssh_file_delete", {
|
|
237
|
+
title: "Delete SSH project file",
|
|
238
|
+
description: "Delete one project file after sha256 verification. Requires profile ALLOW_DELETE=true.",
|
|
239
|
+
inputSchema: {
|
|
240
|
+
profileName: z.string().min(1),
|
|
241
|
+
path: projectPathSchema,
|
|
242
|
+
projectRoot: projectRootSchema,
|
|
243
|
+
expectedSha256: z.string().length(64),
|
|
244
|
+
},
|
|
245
|
+
annotations: { destructiveHint: true },
|
|
246
|
+
}, async (args) => toToolResult(async () => {
|
|
247
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, true);
|
|
248
|
+
try {
|
|
249
|
+
return { ok: true, edit: await deleteProjectFile(workspace, args.path, args.expectedSha256) };
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
await sessions.disconnect(workspace.sessionId);
|
|
253
|
+
}
|
|
254
|
+
}));
|
|
255
|
+
registerTool("ssh_file_move", {
|
|
256
|
+
title: "Move SSH project file",
|
|
257
|
+
description: "Move or rename one project file inside the project root. Requires profile ALLOW_EDIT=true.",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
profileName: z.string().min(1),
|
|
260
|
+
fromPath: projectPathSchema,
|
|
261
|
+
toPath: projectPathSchema,
|
|
262
|
+
projectRoot: projectRootSchema,
|
|
263
|
+
expectedSha256: z.string().length(64),
|
|
264
|
+
createDirs: z.boolean().default(false),
|
|
265
|
+
},
|
|
266
|
+
annotations: { destructiveHint: true },
|
|
267
|
+
}, async (args) => toToolResult(async () => {
|
|
268
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, true);
|
|
269
|
+
try {
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
edit: await moveProjectFile(workspace, args.fromPath, args.toPath, args.expectedSha256, args.createDirs),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
await sessions.disconnect(workspace.sessionId);
|
|
277
|
+
}
|
|
278
|
+
}));
|
|
279
|
+
registerTool("ssh_project_diff", {
|
|
280
|
+
title: "Read SSH project diff",
|
|
281
|
+
description: "Return git status and diff for the remote project when it is a git repository.",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
profileName: z.string().min(1),
|
|
284
|
+
projectRoot: projectRootSchema,
|
|
285
|
+
paths: z.array(projectPathSchema).default([]),
|
|
286
|
+
timeoutMs: z.number().int().min(1).optional(),
|
|
287
|
+
maxOutputBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional(),
|
|
288
|
+
},
|
|
289
|
+
}, async (args) => toToolResult(async () => {
|
|
290
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
291
|
+
try {
|
|
292
|
+
const config = runtimeConfig.get();
|
|
293
|
+
const paths = await resolveProjectPathArgs(workspace, args.paths, false);
|
|
294
|
+
const result = await workspace.session.exec({
|
|
295
|
+
command: buildProjectDiffCommand(workspace.rootPath, paths),
|
|
296
|
+
stdinEncoding: "utf8",
|
|
297
|
+
outputEncoding: "utf8",
|
|
298
|
+
timeoutMs: args.timeoutMs ?? config.defaultExecTimeoutMs,
|
|
299
|
+
maxOutputBytes: args.maxOutputBytes ?? config.defaultMaxOutputBytes,
|
|
300
|
+
});
|
|
301
|
+
return { ok: true, projectRoot: workspace.rootPath, result };
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
await sessions.disconnect(workspace.sessionId);
|
|
305
|
+
}
|
|
306
|
+
}));
|
|
307
|
+
registerTool("ssh_project_run_checks", {
|
|
308
|
+
title: "Run SSH project checks",
|
|
309
|
+
description: "Run explicit project verification commands from the project root.",
|
|
310
|
+
inputSchema: {
|
|
311
|
+
profileName: z.string().min(1),
|
|
312
|
+
projectRoot: projectRootSchema,
|
|
313
|
+
checks: z.array(z.string().min(1)).min(1),
|
|
314
|
+
timeoutMs: z.number().int().min(1).optional(),
|
|
315
|
+
maxOutputBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional(),
|
|
316
|
+
},
|
|
317
|
+
}, async (args) => toToolResult(async () => {
|
|
318
|
+
const workspace = await connectProjectWorkspace(args.profileName, args.projectRoot, false);
|
|
319
|
+
try {
|
|
320
|
+
const config = runtimeConfig.get();
|
|
321
|
+
const results = [];
|
|
322
|
+
for (const check of args.checks) {
|
|
323
|
+
results.push(await workspace.session.exec({
|
|
324
|
+
command: buildRemoteCommand(check, workspace.rootPath),
|
|
325
|
+
stdinEncoding: "utf8",
|
|
326
|
+
outputEncoding: "utf8",
|
|
327
|
+
timeoutMs: args.timeoutMs ?? config.defaultExecTimeoutMs,
|
|
328
|
+
maxOutputBytes: args.maxOutputBytes ?? config.defaultMaxOutputBytes,
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
projectRoot: workspace.rootPath,
|
|
334
|
+
passed: results.every((result) => result.exitCode === 0 && !result.timedOut),
|
|
335
|
+
results,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
await sessions.disconnect(workspace.sessionId);
|
|
340
|
+
}
|
|
341
|
+
}));
|
|
342
|
+
registerTool("ssh_edit_history", {
|
|
343
|
+
title: "List SSH edit history",
|
|
344
|
+
description: "List in-memory edit records created by this MCP server process.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
profileName: z.string().min(1).optional(),
|
|
347
|
+
limit: z.number().int().min(1).max(500).default(50),
|
|
348
|
+
},
|
|
349
|
+
}, async (args) => toToolResult(async () => ({
|
|
350
|
+
ok: true,
|
|
351
|
+
edits: [...editHistory.values()]
|
|
352
|
+
.filter((edit) => args.profileName === undefined || edit.profileName === normalizeProfileNameForHistory(args.profileName))
|
|
353
|
+
.slice(-args.limit),
|
|
354
|
+
})));
|
|
355
|
+
registerTool("ssh_edit_rollback", {
|
|
356
|
+
title: "Rollback SSH edit",
|
|
357
|
+
description: "Rollback an edit created by this MCP server process using stored backup paths.",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
profileName: z.string().min(1),
|
|
360
|
+
editId: z.string().min(1),
|
|
361
|
+
},
|
|
362
|
+
annotations: { destructiveHint: true },
|
|
363
|
+
}, async (args) => toToolResult(async () => {
|
|
364
|
+
const edit = editHistory.get(args.editId);
|
|
365
|
+
if (edit === undefined) {
|
|
366
|
+
throw new McpToolError("ssh_edit_not_found", "SSH edit id was not found", { editId: args.editId });
|
|
367
|
+
}
|
|
368
|
+
const workspace = await connectProjectWorkspace(args.profileName, edit.projectRoot, true);
|
|
369
|
+
try {
|
|
370
|
+
return { ok: true, rollback: await rollbackEdit(workspace, edit) };
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
await sessions.disconnect(workspace.sessionId);
|
|
374
|
+
}
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=register-project-tools.js.map
|