host-execution-mcp 1.0.1 → 2.0.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/CHANGELOG.md +37 -0
- package/README.md +13 -11
- package/SECURITY.md +3 -3
- package/dist/access/index.d.ts +21 -10
- package/dist/access/index.js +55 -27
- package/dist/access/index.js.map +1 -1
- package/dist/access/paradi-auth.d.ts +3 -3
- package/dist/access/paradi-auth.js +4 -4
- package/dist/access/paradi-auth.js.map +1 -1
- package/dist/access/paradi-transport.d.ts +1 -1
- package/dist/access/paradi-transport.js +3 -3
- package/dist/access/paradi-transport.js.map +1 -1
- package/dist/access/paradi-username.d.ts +1 -1
- package/dist/access/paradi-username.js +2 -2
- package/dist/access/paradi-username.js.map +1 -1
- package/dist/access/session-pool.d.ts +5 -0
- package/dist/access/session-pool.js +7 -0
- package/dist/access/session-pool.js.map +1 -1
- package/dist/access/ssh-transport.d.ts +1 -1
- package/dist/access/ssh-transport.js +3 -2
- package/dist/access/ssh-transport.js.map +1 -1
- package/dist/config-yaml.d.ts +54 -0
- package/dist/config-yaml.js +172 -0
- package/dist/config-yaml.js.map +1 -0
- package/dist/config.d.ts +45 -1
- package/dist/config.js +126 -22
- package/dist/config.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +4 -1
- package/dist/instructions.js +73 -51
- package/dist/instructions.js.map +1 -1
- package/dist/kubectl/runner.d.ts +7 -8
- package/dist/kubectl/runner.js +18 -21
- package/dist/kubectl/runner.js.map +1 -1
- package/dist/link-runtime.d.ts +39 -0
- package/dist/link-runtime.js +134 -0
- package/dist/link-runtime.js.map +1 -0
- package/dist/server.d.ts +2 -1
- package/dist/server.js +10 -12
- package/dist/server.js.map +1 -1
- package/dist/sources/guides.d.ts +34 -0
- package/dist/sources/guides.js +50 -0
- package/dist/sources/guides.js.map +1 -0
- package/dist/sources/index.d.ts +5 -0
- package/dist/sources/index.js +22 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/sources/yaml-template.d.ts +31 -0
- package/dist/sources/yaml-template.js +35 -0
- package/dist/sources/yaml-template.js.map +1 -0
- package/dist/ssh/access-client.d.ts +6 -2
- package/dist/ssh/access-client.js +58 -27
- package/dist/ssh/access-client.js.map +1 -1
- package/dist/ssh/access-errors.js +1 -1
- package/dist/ssh/access-errors.js.map +1 -1
- package/dist/ssh/access-sftp.js +3 -3
- package/dist/ssh/access-sftp.js.map +1 -1
- package/dist/tools/copy-job-schemas.d.ts +96 -581
- package/dist/tools/copy-job-schemas.js +9 -9
- package/dist/tools/copy-job-schemas.js.map +1 -1
- package/dist/tools/register.d.ts +2 -10
- package/dist/tools/register.js +174 -331
- package/dist/tools/register.js.map +1 -1
- package/dist/tools/remote-schemas.d.ts +156 -708
- package/dist/tools/remote-schemas.js +99 -103
- package/dist/tools/remote-schemas.js.map +1 -1
- package/dist/utils/copy-executor.d.ts +3 -1
- package/dist/utils/copy-executor.js +39 -25
- package/dist/utils/copy-executor.js.map +1 -1
- package/dist/utils/copy-transfer.d.ts +2 -0
- package/dist/utils/copy-transfer.js.map +1 -1
- package/dist/utils/remote-executor.d.ts +3 -4
- package/dist/utils/remote-executor.js +8 -18
- package/dist/utils/remote-executor.js.map +1 -1
- package/dist/utils/remote-target.js +1 -1
- package/dist/utils/remote-target.js.map +1 -1
- package/docs/AGENT_GUIDE.md +31 -103
- package/docs/CONFIGURATION.md +30 -9
- package/docs/GUIDE.md +186 -0
- package/docs/PARADI_ACCESS_PLAN.md +6 -6
- package/docs/TODO.md +4 -11
- package/docs/USER_GUIDE.zh-CN.md +26 -173
- package/docs/host-execution.yaml +92 -0
- package/package.json +3 -1
package/dist/tools/register.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import { CopyJobManager } from "../utils/copy-job-manager.js";
|
|
2
|
+
import { executeRemoteCopy } from "../utils/copy-executor.js";
|
|
4
3
|
import { buildPsCommand, buildRemoteReadCommand, parseReadFileMeta, } from "../utils/remote-target.js";
|
|
5
4
|
import { formatCommandResult, formatCopyFileResult, formatErrorResult, formatReadFileResult, toToolText, } from "../utils/result.js";
|
|
6
|
-
import { remoteCopyCancelSchema, remoteCopyStatusSchema,
|
|
7
|
-
import {
|
|
5
|
+
import { remoteCopyCancelSchema, remoteCopyStatusSchema, remoteCopyStartSchema, } from "./copy-job-schemas.js";
|
|
6
|
+
import { assertCopySkipVerify, hopHostField, linkDescriptionField, materializeAgentCopyEndpoint, remoteCopyFileSchema, remotePsSchema, remoteReadFileSchema, resolveReadOptions, runTargetFromArgs, } from "./remote-schemas.js";
|
|
8
7
|
async function handleTool(config, fn) {
|
|
9
8
|
const started = Date.now();
|
|
10
9
|
try {
|
|
@@ -21,15 +20,59 @@ async function handleTool(config, fn) {
|
|
|
21
20
|
return toToolText(formatErrorResult(config, error, Date.now() - started));
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
|
-
|
|
23
|
+
function openMachine(hub, pick) {
|
|
24
|
+
const session = hub.resolve({ description: pick.description });
|
|
25
|
+
const ctx = session.access.selectMachine({
|
|
26
|
+
host: pick.host,
|
|
27
|
+
user: pick.user,
|
|
28
|
+
password: pick.password,
|
|
29
|
+
});
|
|
30
|
+
return { session, contextKey: ctx.key };
|
|
31
|
+
}
|
|
32
|
+
/** Catch pick/select errors as tool JSON (same shape as handleTool), not MCP protocol faults. */
|
|
33
|
+
async function handleMachineTool(hub, pick, fn) {
|
|
25
34
|
const started = Date.now();
|
|
26
|
-
const resolved = resolveReadOptions(args);
|
|
27
35
|
try {
|
|
36
|
+
const { session, contextKey } = openMachine(hub, pick);
|
|
37
|
+
return await handleTool(session.config, () => fn(session, contextKey));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function machineSelectFields(config) {
|
|
44
|
+
const targetUserConfigured = Boolean(config?.targetUser?.trim());
|
|
45
|
+
const targetPasswordConfigured = Boolean(config?.targetPassword);
|
|
46
|
+
const isParadi = config?.accessKind === "paradi-gateway";
|
|
47
|
+
return {
|
|
48
|
+
description: linkDescriptionField,
|
|
49
|
+
host: hopHostField,
|
|
50
|
+
user: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(targetUserConfigured
|
|
54
|
+
? `Omit to use TARGET_USER=${config?.targetUser}`
|
|
55
|
+
: "Required for a hop-2 / Paradi host unless the selected link has TARGET_USER"),
|
|
56
|
+
password: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe(isParadi
|
|
60
|
+
? "Ignored for Paradi default auth (gateway password is ACCESS_PASSWORD)"
|
|
61
|
+
: targetPasswordConfigured
|
|
62
|
+
? "Omit to use TARGET_PASSWORD"
|
|
63
|
+
: "Hop-2 password when the selected link has no TARGET_PASSWORD"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function handleRemoteReadFile(hub, args) {
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
try {
|
|
69
|
+
const { session, contextKey } = openMachine(hub, args);
|
|
70
|
+
const resolved = resolveReadOptions(args);
|
|
28
71
|
const command = buildRemoteReadCommand(args.path, resolved);
|
|
29
|
-
const raw = await remote.runOnTarget(
|
|
72
|
+
const raw = await session.remote.runOnTarget(runTargetFromArgs(contextKey, args), command, args.timeoutMs);
|
|
30
73
|
const mergedStdout = [raw.stdout, raw.stderr].filter(Boolean).join("\n");
|
|
31
74
|
const { meta, content } = parseReadFileMeta(mergedStdout);
|
|
32
|
-
return toToolText(formatReadFileResult(config, {
|
|
75
|
+
return toToolText(formatReadFileResult(session.config, {
|
|
33
76
|
content,
|
|
34
77
|
exitCode: raw.exitCode,
|
|
35
78
|
durationMs: raw.durationMs ?? Date.now() - started,
|
|
@@ -39,168 +82,90 @@ async function handleRemoteReadFile(config, remote, args) {
|
|
|
39
82
|
}));
|
|
40
83
|
}
|
|
41
84
|
catch (error) {
|
|
42
|
-
return toToolText(formatErrorResult(
|
|
85
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
43
86
|
}
|
|
44
87
|
}
|
|
45
|
-
function resolveCopyEndpoints(
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
88
|
+
function resolveCopyEndpoints(hub, args) {
|
|
89
|
+
assertCopySkipVerify(args);
|
|
90
|
+
const { session, contextKey } = openMachine(hub, args);
|
|
91
|
+
return {
|
|
92
|
+
session,
|
|
93
|
+
source: materializeAgentCopyEndpoint(session.access, contextKey, args.source),
|
|
94
|
+
dest: materializeAgentCopyEndpoint(session.access, contextKey, args.dest),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function sessionlessConfig() {
|
|
98
|
+
return {
|
|
99
|
+
accessKind: "ssh",
|
|
100
|
+
accessHost: "",
|
|
101
|
+
accessUser: "",
|
|
102
|
+
accessPort: 22,
|
|
103
|
+
defaultTimeoutMs: 60_000,
|
|
104
|
+
maxOutputChars: 65_536,
|
|
105
|
+
};
|
|
52
106
|
}
|
|
53
107
|
export function registerTools(server, deps) {
|
|
54
|
-
const {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const targetPasswordConfigured = Boolean(config.targetPassword);
|
|
58
|
-
const targetHostConfigured = Boolean(config.targetHost?.trim());
|
|
59
|
-
const isParadi = config.accessKind === "paradi-gateway";
|
|
60
|
-
const allowContext = !isParadi;
|
|
61
|
-
const remoteReadFileSchema = buildRemoteReadFileSchema({ allowContext });
|
|
62
|
-
const remotePsSchema = buildRemotePsSchema({ allowContext });
|
|
63
|
-
const remoteCopyFileSchema = buildRemoteCopyFileSchema({ allowContext });
|
|
64
|
-
const remoteCopyStartSchema = buildRemoteCopyStartSchema({ allowContext });
|
|
65
|
-
const contextKeyField = allowContext
|
|
66
|
-
? z
|
|
67
|
-
.string()
|
|
68
|
-
.optional()
|
|
69
|
-
.describe("HostExecutionContext key from context_derive (omit for landing Context)")
|
|
70
|
-
: undefined;
|
|
71
|
-
const shellInputSchema = {
|
|
72
|
-
command: z.string().min(1).describe("Shell command to run on the selected Context"),
|
|
73
|
-
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
|
|
74
|
-
cwd: z.string().optional().describe("Working directory on the Context"),
|
|
75
|
-
...(contextKeyField ? { contextKey: contextKeyField } : {}),
|
|
76
|
-
};
|
|
108
|
+
const { hub } = deps;
|
|
109
|
+
const config = hub.hintConfig();
|
|
110
|
+
const select = machineSelectFields(config);
|
|
77
111
|
server.registerTool("access_check", {
|
|
78
|
-
description:
|
|
79
|
-
? `Probe Local → Paradi gateway → TARGET connectivity (≤10s). Structured JSON: reachable, latencyMs, host, port, auth.
|
|
112
|
+
description: `Probe Local → ACCESS SSH connectivity with short timeouts (≤10s). Structured JSON: reachable, latencyMs, host, port, auth.
|
|
80
113
|
|
|
81
|
-
|
|
114
|
+
AccessTransport only (MCP client → SSH → ACCESS). Does NOT run kubectl or touch hop-2 hosts. Uses an ephemeral SSH session.
|
|
82
115
|
|
|
83
116
|
When to use: first session; VPN/network unsure; after ACCESS_UNAVAILABLE.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
If reachable=false (code ACCESS_UNAVAILABLE): stop retrying other tools; tell user to check VPN / ACCESS_HOST (hints in message). Negative TTL may return a cached failure without re-probing.
|
|
92
|
-
Example: {}`,
|
|
93
|
-
inputSchema: {},
|
|
94
|
-
}, async () => {
|
|
117
|
+
When NOT to use: cluster/kubectl health (shell_exec cluster-info); pod ops.
|
|
118
|
+
If reachable=false (code ACCESS_UNAVAILABLE): stop THIS YAML link (VPN / ACCESS_HOST). Other descriptions may still work. Negative TTL may return a cached failure without re-probing.
|
|
119
|
+
Example: {} or {"description":"<exact link>"}`,
|
|
120
|
+
inputSchema: {
|
|
121
|
+
description: linkDescriptionField,
|
|
122
|
+
},
|
|
123
|
+
}, async ({ description }) => {
|
|
95
124
|
const started = Date.now();
|
|
96
125
|
try {
|
|
97
|
-
const
|
|
126
|
+
const session = hub.resolve({ description });
|
|
127
|
+
const result = await session.accessClient.check();
|
|
98
128
|
return toToolText({ ...result });
|
|
99
129
|
}
|
|
100
130
|
catch (error) {
|
|
101
|
-
return toToolText(formatErrorResult(
|
|
131
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
102
132
|
}
|
|
103
133
|
});
|
|
104
134
|
server.registerTool("shell_exec", {
|
|
105
|
-
description:
|
|
106
|
-
? `Run a shell command on the Paradi landing Context (TARGET host OS).
|
|
107
|
-
|
|
108
|
-
Architecture: MCP → SSH(gateway, encoded user) → TARGET shell. Omit contextKey.
|
|
135
|
+
description: `Run a shell command on the selected machine.
|
|
109
136
|
|
|
110
|
-
|
|
111
|
-
When NOT to use:
|
|
112
|
-
Example: {"command":"hostname","
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Example: {"command":"kubectl get nodes","timeoutMs":30000} or {"command":"hostname","contextKey":"<from context_derive>"}`,
|
|
121
|
-
inputSchema: shellInputSchema,
|
|
122
|
-
}, async ({ command, contextKey, timeoutMs, cwd }) => handleTool(config, () => access.shellExec(command, {
|
|
137
|
+
Omit host = this YAML link's landing (laptop-direct host, or the jump). Pass host = ssh hop-2 from ACCESS, or Paradi TARGET select.
|
|
138
|
+
When NOT to use: inside a pod (k8s_exec), connectivity probe (access_check).
|
|
139
|
+
Example: {"command":"hostname"} or {"host":"10.0.0.5","command":"hostname"}`,
|
|
140
|
+
inputSchema: {
|
|
141
|
+
command: z.string().min(1).describe("Shell command to run on the selected machine"),
|
|
142
|
+
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
|
|
143
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
144
|
+
...select,
|
|
145
|
+
},
|
|
146
|
+
}, async ({ command, timeoutMs, cwd, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.shellExec(command, {
|
|
123
147
|
contextKey,
|
|
124
|
-
timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
|
|
148
|
+
timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
|
|
125
149
|
cwd,
|
|
126
150
|
})));
|
|
127
|
-
server.registerTool("context_derive", {
|
|
128
|
-
description: isParadi
|
|
129
|
-
? `UNAVAILABLE under ACCESS_KIND=paradi-gateway — landing Context is already TARGET. Handler throws.`
|
|
130
|
-
: `Derive a child HostExecutionContext from landing. Returns contextKey + endpoint + capabilities summary.
|
|
131
|
-
|
|
132
|
-
Use the returned contextKey on shell_exec, docker_*, context_probe, remote_* target=context, and remote_copy_* target=context.
|
|
133
|
-
Omit host to use TARGET_HOST when configured; useTarget=true forces default TARGET derive selector.
|
|
134
|
-
Example: {"host":"10.0.0.5","user":"admin"} or {"useTarget":true}`,
|
|
135
|
-
inputSchema: isParadi
|
|
136
|
-
? {}
|
|
137
|
-
: {
|
|
138
|
-
host: z
|
|
139
|
-
.string()
|
|
140
|
-
.optional()
|
|
141
|
-
.describe(targetHostConfigured
|
|
142
|
-
? `Omit to use TARGET_HOST=${config.targetHost} with useTarget=true`
|
|
143
|
-
: "Target host IP/hostname reachable FROM ACCESS"),
|
|
144
|
-
useTarget: z
|
|
145
|
-
.boolean()
|
|
146
|
-
.optional()
|
|
147
|
-
.describe(targetHostConfigured
|
|
148
|
-
? `When true, derive TARGET_HOST=${config.targetHost}`
|
|
149
|
-
: "When true, derive default TARGET from config (requires TARGET_HOST)"),
|
|
150
|
-
user: z
|
|
151
|
-
.string()
|
|
152
|
-
.optional()
|
|
153
|
-
.describe(targetUserConfigured
|
|
154
|
-
? `Omit to use TARGET_USER=${config.targetUser}`
|
|
155
|
-
: "SSH user (required unless TARGET_USER set)"),
|
|
156
|
-
password: z
|
|
157
|
-
.string()
|
|
158
|
-
.optional()
|
|
159
|
-
.describe(targetPasswordConfigured
|
|
160
|
-
? "Omit to use TARGET_PASSWORD"
|
|
161
|
-
: "SSH password via sshpass (or set TARGET_PASSWORD)"),
|
|
162
|
-
},
|
|
163
|
-
}, async (args) => {
|
|
164
|
-
const started = Date.now();
|
|
165
|
-
try {
|
|
166
|
-
const summary = access.deriveContext(args);
|
|
167
|
-
return toToolText({
|
|
168
|
-
success: true,
|
|
169
|
-
...summary,
|
|
170
|
-
durationMs: Date.now() - started,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
return toToolText(formatErrorResult(config, error, Date.now() - started));
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
151
|
server.registerTool("k8s_list_pods", {
|
|
178
|
-
description: `List pods in a namespace (kubectl
|
|
179
|
-
|
|
180
|
-
By design k8s_* have no contextKey. For kubectl on a derived host use shell_exec with contextKey.
|
|
181
|
-
|
|
182
|
-
No capabilities.kubectl gate — kubectl failures surface via exitCode; use context_probe to check kubectl CLI presence.
|
|
152
|
+
description: `List pods in a namespace (kubectl get pods on the selected machine).
|
|
183
153
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
Prerequisites: namespace from user or context.
|
|
187
|
-
Example: {"namespace":"default","labelSelector":"app=web","fieldSelector":"status.phase=Running","output":"wide"}`,
|
|
154
|
+
No capabilities.kubectl gate — failures surface via exitCode; use context_probe for CLI presence.
|
|
155
|
+
Example: {"namespace":"default","labelSelector":"app=web","output":"wide"}`,
|
|
188
156
|
inputSchema: {
|
|
189
157
|
namespace: z.string().min(1),
|
|
190
158
|
labelSelector: z.string().optional(),
|
|
191
159
|
fieldSelector: z.string().optional(),
|
|
192
160
|
output: z.enum(["wide", "json", "name"]).optional(),
|
|
193
161
|
timeoutMs: z.number().int().positive().optional(),
|
|
162
|
+
...select,
|
|
194
163
|
},
|
|
195
|
-
}, async ({ namespace, labelSelector, fieldSelector, output, timeoutMs }) =>
|
|
164
|
+
}, async ({ namespace, labelSelector, fieldSelector, output, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.kubectl.listPods(namespace, { labelSelector, fieldSelector, output }, timeoutMs, contextKey)));
|
|
196
165
|
server.registerTool("k8s_exec", {
|
|
197
|
-
description: `Run a command inside a pod container (
|
|
166
|
+
description: `Run a command inside a pod container (kubectl exec on the selected machine, non-interactive).
|
|
198
167
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
When to use: run scripts, inspect env, list dirs, start processes inside a container.
|
|
202
|
-
When NOT to use: file logs on disk (remote_read_file mode=tail), container stdout history (k8s_logs), host OS via SSH (ssh_exec).
|
|
203
|
-
Prerequisites: namespace and pod — use k8s_list_pods first if unknown.
|
|
168
|
+
When to use: scripts, env, dirs inside a container. Not file logs (remote_read_file) or container stdout history (k8s_logs).
|
|
204
169
|
Example: {"namespace":"dev","pod":"web-abc","command":"ls -la /app","container":"app"}`,
|
|
205
170
|
inputSchema: {
|
|
206
171
|
namespace: z.string().min(1),
|
|
@@ -208,14 +173,13 @@ Example: {"namespace":"dev","pod":"web-abc","command":"ls -la /app","container":
|
|
|
208
173
|
command: z.string().min(1),
|
|
209
174
|
container: z.string().optional(),
|
|
210
175
|
timeoutMs: z.number().int().positive().optional(),
|
|
176
|
+
...select,
|
|
211
177
|
},
|
|
212
|
-
}, async ({ namespace, pod, command, container, timeoutMs }) =>
|
|
178
|
+
}, async ({ namespace, pod, command, container, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.kubectl.exec(namespace, pod, command, container, timeoutMs, contextKey)));
|
|
213
179
|
server.registerTool("k8s_logs", {
|
|
214
|
-
description: `Fetch pod container stdout/stderr (
|
|
180
|
+
description: `Fetch pod container stdout/stderr (kubectl logs on the selected machine).
|
|
215
181
|
|
|
216
|
-
When to use:
|
|
217
|
-
When NOT to use: log files on disk (remote_read_file mode=tail). Often empty for sshd-only idle pods.
|
|
218
|
-
Prerequisites: namespace and pod name.
|
|
182
|
+
When to use: container runtime logs. Not log files on disk (remote_read_file). Often empty for sshd-only idle pods.
|
|
219
183
|
Example: {"namespace":"dev","pod":"web-abc","tail":200,"since":"1h"}`,
|
|
220
184
|
inputSchema: {
|
|
221
185
|
namespace: z.string().min(1),
|
|
@@ -224,97 +188,28 @@ Example: {"namespace":"dev","pod":"web-abc","tail":200,"since":"1h"}`,
|
|
|
224
188
|
tail: z.number().int().positive().optional(),
|
|
225
189
|
since: z.string().optional(),
|
|
226
190
|
timeoutMs: z.number().int().positive().optional(),
|
|
191
|
+
...select,
|
|
227
192
|
},
|
|
228
|
-
}, async ({ namespace, pod, container, tail, since, timeoutMs }) =>
|
|
229
|
-
server.registerTool("ssh_exec", {
|
|
230
|
-
description: isParadi
|
|
231
|
-
? `UNAVAILABLE under ACCESS_KIND=paradi-gateway — landing Context is already TARGET. Use shell_exec for TARGET shell, docker_* (omit contextKey), or k8s_* for pods. Handler errors if called.`
|
|
232
|
-
: `Shortcut: context_derive + shell_exec on a remote host (ACCESS → ssh/sshpass → host).
|
|
233
|
-
|
|
234
|
-
IMPORTANT: NOT SSH from the laptop. MCP → ACCESS; ACCESS derives and shells to the host.
|
|
235
|
-
Returns reusable contextKey + standard command result fields (exitCode, stdout, etc.). Prefer this for derive+shell in one call; reuse contextKey for follow-up shell_exec / docker_* / remote_* target=context.
|
|
236
|
-
|
|
237
|
-
When to use: host IP/node when kubectl exec is not appropriate; omit host to use TARGET_HOST when configured.
|
|
238
|
-
When NOT to use: inside a pod (k8s_exec); ACCESS_KIND=paradi-gateway (unsupported — handler throws).
|
|
239
|
-
Example: {"host":"10.0.0.5","command":"hostname"} or {"command":"hostname"} with TARGET_HOST set`,
|
|
240
|
-
inputSchema: {
|
|
241
|
-
host: z
|
|
242
|
-
.string()
|
|
243
|
-
.optional()
|
|
244
|
-
.describe(isParadi
|
|
245
|
-
? "UNAVAILABLE under paradi-gateway"
|
|
246
|
-
: targetHostConfigured
|
|
247
|
-
? `Omit to use TARGET_HOST=${config.targetHost}`
|
|
248
|
-
: "Target host IP/hostname reachable FROM ACCESS (required unless TARGET_HOST set)"),
|
|
249
|
-
command: z.string().min(1),
|
|
250
|
-
user: z
|
|
251
|
-
.string()
|
|
252
|
-
.optional()
|
|
253
|
-
.describe(isParadi
|
|
254
|
-
? "UNAVAILABLE under paradi-gateway"
|
|
255
|
-
: targetUserConfigured
|
|
256
|
-
? `Omit to use TARGET_USER=${config.targetUser}`
|
|
257
|
-
: "SSH user (required unless TARGET_USER set)"),
|
|
258
|
-
password: z
|
|
259
|
-
.string()
|
|
260
|
-
.optional()
|
|
261
|
-
.describe(isParadi
|
|
262
|
-
? "UNAVAILABLE under paradi-gateway"
|
|
263
|
-
: targetPasswordConfigured
|
|
264
|
-
? "Omit to use TARGET_PASSWORD"
|
|
265
|
-
: "SSH password via sshpass (or set TARGET_PASSWORD)"),
|
|
266
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
267
|
-
},
|
|
268
|
-
}, async ({ host, command, user, password, timeoutMs }) => {
|
|
269
|
-
const started = Date.now();
|
|
270
|
-
try {
|
|
271
|
-
const raw = await access.sshExec({
|
|
272
|
-
host,
|
|
273
|
-
command,
|
|
274
|
-
user,
|
|
275
|
-
password,
|
|
276
|
-
timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
|
|
277
|
-
});
|
|
278
|
-
const mergedStdout = [raw.stdout, raw.stderr].filter(Boolean).join("\n");
|
|
279
|
-
const base = formatCommandResult(config, {
|
|
280
|
-
stdout: mergedStdout,
|
|
281
|
-
stderr: "",
|
|
282
|
-
exitCode: raw.exitCode,
|
|
283
|
-
durationMs: raw.durationMs ?? Date.now() - started,
|
|
284
|
-
});
|
|
285
|
-
return toToolText({
|
|
286
|
-
...base,
|
|
287
|
-
contextKey: raw.contextKey,
|
|
288
|
-
endpoint: raw.endpoint,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
return toToolText(formatErrorResult(config, error, Date.now() - started));
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
const withOptionalContextKey = (fields) => contextKeyField ? { ...fields, contextKey: contextKeyField } : fields;
|
|
193
|
+
}, async ({ namespace, pod, container, tail, since, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.kubectl.logs(namespace, pod, { container, tail, since }, timeoutMs, contextKey)));
|
|
296
194
|
server.registerTool("context_probe", {
|
|
297
|
-
description:
|
|
298
|
-
? `Probe TARGET landing capabilities (docker/kubectl CLI). SSH success ≠ docker/kubectl.
|
|
299
|
-
Example: {}`
|
|
300
|
-
: `Probe Context capabilities (docker/kubectl CLI presence). SSH success does NOT imply docker/kubectl.
|
|
195
|
+
description: `Probe docker/kubectl CLI on the selected machine. SSH success ≠ docker/kubectl.
|
|
301
196
|
|
|
302
|
-
Returns
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
inputSchema: withOptionalContextKey({
|
|
197
|
+
Returns true | false | unknown. Prefer this (or let docker_* auto-probe) before assuming docker.
|
|
198
|
+
Example: {} or {"host":"10.0.0.5"}`,
|
|
199
|
+
inputSchema: {
|
|
306
200
|
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
|
|
307
|
-
|
|
308
|
-
|
|
201
|
+
...select,
|
|
202
|
+
},
|
|
203
|
+
}, async ({ timeoutMs, description, host, user, password }) => {
|
|
309
204
|
const started = Date.now();
|
|
310
205
|
try {
|
|
311
|
-
const
|
|
206
|
+
const { session, contextKey } = openMachine(hub, { description, host, user, password });
|
|
207
|
+
const result = await session.access.probe({
|
|
312
208
|
contextKey,
|
|
313
|
-
timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
|
|
209
|
+
timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
|
|
314
210
|
});
|
|
315
211
|
return toToolText({
|
|
316
212
|
success: true,
|
|
317
|
-
contextKey: result.contextKey,
|
|
318
213
|
endpoint: result.endpoint,
|
|
319
214
|
capabilities: result.capabilities,
|
|
320
215
|
durationMs: Date.now() - started,
|
|
@@ -322,120 +217,81 @@ Example: {} or {"contextKey":"<from context_derive>"}`,
|
|
|
322
217
|
});
|
|
323
218
|
}
|
|
324
219
|
catch (error) {
|
|
325
|
-
return toToolText(formatErrorResult(
|
|
220
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
326
221
|
}
|
|
327
222
|
});
|
|
328
223
|
server.registerTool("docker_ps", {
|
|
329
|
-
description:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
: `List Docker containers on a HostExecutionContext (landing or derived via contextKey).
|
|
333
|
-
|
|
334
|
-
Runtime only — uses SessionPool; does not create SSH. Auto-probes capabilities.docker if unknown; fails if false.
|
|
335
|
-
Example: {"all":true} or {"contextKey":"<from context_derive>","all":true}`,
|
|
336
|
-
inputSchema: withOptionalContextKey({
|
|
224
|
+
description: `List Docker containers on the selected machine (auto-probes docker).
|
|
225
|
+
Example: {"all":true} or {"host":"10.0.0.5","all":true}`,
|
|
226
|
+
inputSchema: {
|
|
337
227
|
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
|
|
338
228
|
all: z.boolean().optional().describe("Include stopped containers (-a)"),
|
|
339
229
|
quiet: z.boolean().optional().describe("Only container IDs (-q)"),
|
|
340
230
|
filter: z.string().optional().describe("docker ps --filter value"),
|
|
341
|
-
|
|
342
|
-
|
|
231
|
+
...select,
|
|
232
|
+
},
|
|
233
|
+
}, async ({ timeoutMs, all, quiet, filter, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.dockerPs({
|
|
343
234
|
contextKey,
|
|
344
235
|
all,
|
|
345
236
|
quiet,
|
|
346
237
|
filter,
|
|
347
|
-
timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
|
|
238
|
+
timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
|
|
348
239
|
})));
|
|
349
240
|
server.registerTool("docker_exec", {
|
|
350
|
-
description:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
: `Run a command inside a Docker container on landing or derived Context (docker exec → bash -lc).
|
|
354
|
-
|
|
355
|
-
Not kubectl exec (use k8s_exec for pods). Auto-probes docker capability.
|
|
356
|
-
Example: {"container":"musa-starvla","command":"nvidia-smi"} or {"contextKey":"<key>","container":"c1","command":"pwd"}`,
|
|
357
|
-
inputSchema: withOptionalContextKey({
|
|
241
|
+
description: `Run a command inside a Docker container (docker exec → bash -lc). Not kubectl exec.
|
|
242
|
+
Example: {"container":"app","command":"pwd"} or {"host":"10.0.0.5","container":"c1","command":"pwd"}`,
|
|
243
|
+
inputSchema: {
|
|
358
244
|
container: z.string().min(1).describe("Container name or ID"),
|
|
359
245
|
command: z.string().min(1).describe("Command run via bash -lc inside the container"),
|
|
360
246
|
workdir: z.string().optional().describe("docker exec -w"),
|
|
361
247
|
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
|
|
362
|
-
|
|
363
|
-
|
|
248
|
+
...select,
|
|
249
|
+
},
|
|
250
|
+
}, async ({ container, command, workdir, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.dockerExec({
|
|
364
251
|
container,
|
|
365
252
|
command,
|
|
366
253
|
workdir,
|
|
367
254
|
contextKey,
|
|
368
|
-
timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
|
|
255
|
+
timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
|
|
369
256
|
})));
|
|
370
257
|
server.registerTool("docker_logs", {
|
|
371
|
-
description:
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
: `Fetch Docker container logs (docker logs) on landing or derived Context.
|
|
375
|
-
|
|
376
|
-
For k8s pod logs use k8s_logs. Auto-probes docker capability.
|
|
377
|
-
Example: {"container":"musa-starvla","tail":200}`,
|
|
378
|
-
inputSchema: withOptionalContextKey({
|
|
258
|
+
description: `Fetch Docker container logs on the selected machine. For k8s pod logs use k8s_logs.
|
|
259
|
+
Example: {"container":"app","tail":200}`,
|
|
260
|
+
inputSchema: {
|
|
379
261
|
container: z.string().min(1),
|
|
380
262
|
tail: z.number().int().positive().optional().describe("Default 200"),
|
|
381
263
|
since: z.string().optional().describe("docker logs --since"),
|
|
382
264
|
timestamps: z.boolean().optional(),
|
|
383
265
|
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
|
|
384
|
-
|
|
385
|
-
|
|
266
|
+
...select,
|
|
267
|
+
},
|
|
268
|
+
}, async ({ container, tail, since, timestamps, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.dockerLogs({
|
|
386
269
|
container,
|
|
387
270
|
tail,
|
|
388
271
|
since,
|
|
389
272
|
timestamps,
|
|
390
273
|
contextKey,
|
|
391
|
-
timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
|
|
274
|
+
timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
|
|
392
275
|
})));
|
|
393
276
|
server.registerTool("remote_read_file", {
|
|
394
|
-
description:
|
|
395
|
-
? `Read a file on TARGET landing (target=landing) or in a pod (target=pod).
|
|
396
|
-
|
|
397
|
-
Schema rejects target=context under Paradi — use target=landing for OS files.
|
|
398
|
-
Use mode=head|tail. Limits: MAX_OUTPUT_CHARS. For transfer use remote_copy_file.
|
|
399
|
-
Example: {"target":"landing","path":"/var/log/app.log","mode":"tail","lines":100}`
|
|
400
|
-
: `Read a file on landing, in a pod, or on a derived Context (target=context).
|
|
401
|
-
|
|
402
|
-
Use target to select path: landing=shell, pod=kubectl exec, context=derived shell (contextKey required).
|
|
403
|
-
Use mode to read from start or end of file:
|
|
404
|
-
- mode=head (default): first N lines; default lines=500; supports offset for pagination
|
|
405
|
-
- mode=tail: last N lines; default lines=200; supports grep filter
|
|
406
|
-
|
|
407
|
-
Limits: MAX_OUTPUT_CHARS env (default 65536) may truncate response; check truncatedByChars.
|
|
408
|
-
Response includes totalLines/totalBytes/truncatedByLines. For file transfer use remote_copy_file.
|
|
277
|
+
description: `Read a file on the selected machine (description + optional host). Add namespace+pod to read inside a container via kubectl on that machine.
|
|
409
278
|
|
|
279
|
+
mode=head (default, lines=500, offset) or mode=tail (lines=200, grep).
|
|
410
280
|
When NOT to use: container stdout (k8s_logs); copying files (remote_copy_file).
|
|
411
|
-
Example
|
|
412
|
-
Example (context): {"target":"context","contextKey":"<key>","path":"/etc/hostname"}`,
|
|
281
|
+
Example: {"path":"/etc/hostname"} or {"host":"10.0.0.5","path":"/var/log/app.log","mode":"tail"}`,
|
|
413
282
|
inputSchema: remoteReadFileSchema,
|
|
414
|
-
}, async (args) => handleRemoteReadFile(
|
|
283
|
+
}, async (args) => handleRemoteReadFile(hub, args));
|
|
415
284
|
server.registerTool("remote_copy_file", {
|
|
416
|
-
description:
|
|
417
|
-
? `Synchronous copy between local, landing (=TARGET), or pod. Schema rejects target=context under Paradi.
|
|
418
|
-
|
|
419
|
-
local↔landing uses SFTP (declared degraded — prefer smaller transfers / remote_copy_start; expect retries).
|
|
420
|
-
Example: {"source":{"target":"local","path":"C:/tmp/a.bin"},"dest":{"target":"landing","path":"/tmp/a.bin"}}`
|
|
421
|
-
: `Synchronous copy of a file or directory between local, landing, pod, or context.
|
|
422
|
-
|
|
423
|
-
Always waits for completion and returns success/bytes/transferId — does NOT start a background job.
|
|
424
|
-
- local↔landing: independent SFTP (ssh2-sftp-client); files and directories (directory = walk + per-file)
|
|
425
|
-
- landing/pod/context (context materializes before copy): kubectl cp, scp, cp on landing
|
|
285
|
+
description: `Synchronous copy. Endpoints: target=local (MCP machine), target=remote (selected Linux), target=pod (kubectl on that Linux).
|
|
426
286
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
When to use: short/medium sync copies that fit the MCP call timeout.
|
|
431
|
-
When NOT to use: long/large copies — call remote_copy_start (async) then remote_copy_status (size>256MB or ETA>2min). Do not expect remote_copy_file to auto-async.
|
|
432
|
-
Example (file): {"source":{"target":"local","path":"C:/tmp/a.bin"},"dest":{"target":"landing","path":"/tmp/a.bin"}}
|
|
433
|
-
Example (context): {"source":{"target":"context","contextKey":"<key>","path":"/data/a.bin"},"dest":{"target":"local","path":"C:/data/a.bin"}}`,
|
|
287
|
+
Select the Linux with description + optional host (same as shell_exec). Paradi: no hop-2.
|
|
288
|
+
Large/long copies: remote_copy_start, not this tool.
|
|
289
|
+
Example: {"source":{"target":"remote","path":"/etc/hostname"},"dest":{"target":"local","path":"C:/tmp/hostname"}}`,
|
|
434
290
|
inputSchema: remoteCopyFileSchema,
|
|
435
291
|
}, async (args, extra) => {
|
|
436
292
|
const started = Date.now();
|
|
437
293
|
try {
|
|
438
|
-
const { source, dest } = resolveCopyEndpoints(
|
|
294
|
+
const { session, source, dest } = resolveCopyEndpoints(hub, args);
|
|
439
295
|
const progressToken = extra._meta?.progressToken;
|
|
440
296
|
const onProgress = (progress) => {
|
|
441
297
|
if (progressToken === undefined) {
|
|
@@ -452,7 +308,7 @@ Example (context): {"source":{"target":"context","contextKey":"<key>","path":"/d
|
|
|
452
308
|
})
|
|
453
309
|
.catch(() => undefined);
|
|
454
310
|
};
|
|
455
|
-
const result = await executeRemoteCopy({ accessClient, config }, source, dest, {
|
|
311
|
+
const result = await executeRemoteCopy({ accessClient: session.accessClient, config: session.config }, source, dest, {
|
|
456
312
|
recursive: args.recursive ?? true,
|
|
457
313
|
timeoutMs: args.timeoutMs,
|
|
458
314
|
skipIfComplete: args.skipIfComplete,
|
|
@@ -479,27 +335,20 @@ Example (context): {"source":{"target":"context","contextKey":"<key>","path":"/d
|
|
|
479
335
|
}));
|
|
480
336
|
}
|
|
481
337
|
catch (error) {
|
|
482
|
-
return toToolText(formatErrorResult(
|
|
338
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
483
339
|
}
|
|
484
340
|
});
|
|
485
341
|
server.registerTool("remote_copy_start", {
|
|
486
|
-
description:
|
|
487
|
-
? `Start an async copy job and return transferId immediately. Same rules as remote_copy_file under Paradi: landing=TARGET; schema rejects target=context. SFTP degraded — prefer this for large transfers.
|
|
488
|
-
Example: {"source":{"target":"landing","path":"/tmp/big.pt"},"dest":{"target":"local","path":"C:/data/big.pt"},"skipIfComplete":true,"totalTimeoutMs":7200000}`
|
|
489
|
-
: `Start an async file/directory copy job (same path engine as remote_copy_file) and return immediately with transferId.
|
|
342
|
+
description: `Start an async copy job (same rules as remote_copy_file) and return transferId immediately.
|
|
490
343
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
Poll remote_copy_status; cancel with remote_copy_cancel.
|
|
494
|
-
Prefer when size>256MB or ETA>2min. Supports skipIfComplete/verify/totalTimeoutMs.
|
|
495
|
-
Terminal statuses: success|failed|cancelled|timeout.
|
|
496
|
-
Example: {"source":{"target":"landing","path":"/tmp/big.pt"},"dest":{"target":"local","path":"C:/data/big.pt"},"skipIfComplete":true,"totalTimeoutMs":7200000}`,
|
|
344
|
+
Prefer when size>256MB or ETA>2min. Poll remote_copy_status; cancel with remote_copy_cancel.
|
|
345
|
+
Example: {"source":{"target":"remote","path":"/tmp/big.pt"},"dest":{"target":"local","path":"C:/data/big.pt"},"skipIfComplete":true,"totalTimeoutMs":7200000}`,
|
|
497
346
|
inputSchema: remoteCopyStartSchema,
|
|
498
347
|
}, async (args) => {
|
|
499
348
|
const started = Date.now();
|
|
500
349
|
try {
|
|
501
|
-
const { source, dest } = resolveCopyEndpoints(
|
|
502
|
-
const result = await
|
|
350
|
+
const { session, source, dest } = resolveCopyEndpoints(hub, args);
|
|
351
|
+
const result = await hub.copyStart(session, {
|
|
503
352
|
source,
|
|
504
353
|
dest,
|
|
505
354
|
recursive: args.recursive,
|
|
@@ -512,7 +361,7 @@ Example: {"source":{"target":"landing","path":"/tmp/big.pt"},"dest":{"target":"l
|
|
|
512
361
|
return toToolText(result);
|
|
513
362
|
}
|
|
514
363
|
catch (error) {
|
|
515
|
-
return toToolText(formatErrorResult(
|
|
364
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
516
365
|
}
|
|
517
366
|
});
|
|
518
367
|
server.registerTool("remote_copy_status", {
|
|
@@ -524,47 +373,41 @@ Example: {"transferId":"copy-xxx"}`,
|
|
|
524
373
|
}, async (args) => {
|
|
525
374
|
const started = Date.now();
|
|
526
375
|
try {
|
|
527
|
-
const result = await
|
|
376
|
+
const result = await hub.copyStatus(args.transferId);
|
|
528
377
|
return toToolText(result);
|
|
529
378
|
}
|
|
530
379
|
catch (error) {
|
|
531
|
-
return toToolText(formatErrorResult(
|
|
380
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
532
381
|
}
|
|
533
382
|
});
|
|
534
383
|
server.registerTool("remote_copy_cancel", {
|
|
535
384
|
description: `Cancel a running/queued async copy job by transferId.
|
|
536
385
|
|
|
537
|
-
Aborts the in-process worker: local↔
|
|
386
|
+
Aborts the in-process worker: local↔remote SFTP sessions end promptly; kubectl cp / scp on landing is best-effort.
|
|
538
387
|
Already-finished jobs are left unchanged (terminal status is fenced).
|
|
539
388
|
Example: {"transferId":"copy-xxx"}`,
|
|
540
389
|
inputSchema: remoteCopyCancelSchema,
|
|
541
390
|
}, async (args) => {
|
|
542
391
|
const started = Date.now();
|
|
543
392
|
try {
|
|
544
|
-
const result = await
|
|
393
|
+
const result = await hub.copyCancel(args.transferId);
|
|
545
394
|
return toToolText(result);
|
|
546
395
|
}
|
|
547
396
|
catch (error) {
|
|
548
|
-
return toToolText(formatErrorResult(
|
|
397
|
+
return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
|
|
549
398
|
}
|
|
550
399
|
});
|
|
551
400
|
server.registerTool("remote_ps", {
|
|
552
|
-
description:
|
|
553
|
-
|
|
554
|
-
Example: {"target":"landing","pattern":"python"}`
|
|
555
|
-
: `List processes on landing, in a pod, or on a derived Context (ps aux | grep).
|
|
556
|
-
|
|
557
|
-
When NOT to use: reading log files (remote_read_file).
|
|
558
|
-
Example (pod): {"target":"pod","namespace":"dev","pod":"train-0","pattern":"python"}
|
|
559
|
-
Example (context): {"target":"context","contextKey":"<key>","pattern":"nginx"}`,
|
|
401
|
+
description: `List processes on the selected machine (description + optional host). Add namespace+pod to list inside a container.
|
|
402
|
+
Example: {"pattern":"sshd"} or {"host":"10.0.0.5","pattern":"python"}`,
|
|
560
403
|
inputSchema: remotePsSchema,
|
|
561
|
-
}, async (args) =>
|
|
404
|
+
}, async (args) => handleMachineTool(hub, args, async (session, contextKey) => {
|
|
562
405
|
const command = buildPsCommand(args.pattern);
|
|
563
|
-
return remote.runOnTarget(
|
|
406
|
+
return session.remote.runOnTarget(runTargetFromArgs(contextKey, args), command, args.timeoutMs);
|
|
564
407
|
}));
|
|
565
408
|
}
|
|
566
409
|
export function registerPrompts(server) {
|
|
567
|
-
server.prompt("tool-selection", "Recommend the best host-execution-mcp tool for a scenario (all ops go via ACCESS landing
|
|
410
|
+
server.prompt("tool-selection", "Recommend the best host-execution-mcp tool for a scenario (all ops go via ACCESS landing, optional host for hop-2)", {
|
|
568
411
|
scenario: z.string().describe("Description of what the user wants to do remotely"),
|
|
569
412
|
}, async ({ scenario }) => ({
|
|
570
413
|
messages: [
|
|
@@ -573,11 +416,11 @@ export function registerPrompts(server) {
|
|
|
573
416
|
content: {
|
|
574
417
|
type: "text",
|
|
575
418
|
text: `Analyze the user's remote operation scenario and respond with:
|
|
576
|
-
1. Recommended tool (access_check |
|
|
577
|
-
2. Why this tool fits (
|
|
419
|
+
1. Recommended tool (access_check | context_probe | shell_exec | k8s_list_pods | k8s_exec | docker_ps | docker_exec | docker_logs | k8s_logs | remote_read_file | remote_copy_file | remote_copy_start | remote_copy_status | remote_copy_cancel | remote_ps)
|
|
420
|
+
2. Why this tool fits (MCP → ACCESS landing; pass host only for hop-2 / Paradi TARGET select; large copies — remote_copy_start; VPN/network unsure — access_check first)
|
|
578
421
|
3. The first MCP tool call (JSON arguments only)
|
|
579
422
|
|
|
580
|
-
If VPN/connectivity may be down, recommend access_check first. Use k8s_list_pods when pod name is unknown.
|
|
423
|
+
If VPN/connectivity may be down, recommend access_check first. Use k8s_list_pods when pod name is unknown. Prefer k8s_exec for pod/container access.
|
|
581
424
|
|
|
582
425
|
Scenario: ${scenario}`,
|
|
583
426
|
},
|
|
@@ -603,8 +446,8 @@ Log path: ${log_path ?? "(ask user if needed)"}
|
|
|
603
446
|
Process pattern: ${pattern ?? "(optional)"}
|
|
604
447
|
|
|
605
448
|
Steps:
|
|
606
|
-
1. Verify connectivity (access_check); if unreachable stop and report VPN/ACCESS_HOST
|
|
607
|
-
2. Verify cluster (shell_exec kubectl cluster-info)
|
|
449
|
+
1. Verify connectivity (access_check); if this link is unreachable, stop that link and report VPN/ACCESS_HOST
|
|
450
|
+
2. Verify cluster (shell_exec kubectl cluster-info) when kubectl is needed
|
|
608
451
|
3. List pods if needed (k8s_list_pods)
|
|
609
452
|
4. Read or tail files (remote_read_file); copy files (remote_copy_file or remote_copy_start for large)
|
|
610
453
|
5. Check processes (remote_ps)
|