@xpufx/paseo-mcp-tools 0.1.3
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 +84 -0
- package/client/__snapshots__/attribution.test.tsx.snap +170 -0
- package/client/commands.ts +113 -0
- package/client/mcp-query.tsx +39 -0
- package/client/pill.tsx +886 -0
- package/client/timeline.tsx +136 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/docs/TEST_METHODOLOGY.md +29 -0
- package/package.json +41 -0
- package/paseo-plugin.json +4 -0
- package/scripts/version.mjs +4 -0
- package/server/discovery/extract.ts +277 -0
- package/server/discovery/types.ts +23 -0
- package/server/health/health.ts +274 -0
- package/server/inject.ts +32 -0
- package/server/mcp.ts +365 -0
- package/server/providers/README.md +51 -0
- package/server/providers/antigravity.ts +22 -0
- package/server/providers/catalog.ts +6 -0
- package/server/providers/claude.ts +21 -0
- package/server/providers/codex.ts +85 -0
- package/server/providers/family.ts +24 -0
- package/server/providers/index.ts +11 -0
- package/server/providers/opencode.ts +95 -0
- package/server/providers/paseo.ts +112 -0
- package/server/providers/pi.ts +39 -0
- package/server/settings.ts +19 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
- package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
- package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
- package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
- package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
- package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/mcp.ts +207 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { McpClient } from "paseo-plugin-helper/mcp";
|
|
2
|
+
import { withTimeout } from "paseo-plugin-helper/shared";
|
|
3
|
+
import type { McpServer } from "../../shared/mcp";
|
|
4
|
+
|
|
5
|
+
// GTD: generic health check that works with *every* MCP.
|
|
6
|
+
// This is a CLIENT — we are not an MCP server, we just dial MCP servers.
|
|
7
|
+
// Runs on the Paseo daemon (Node, can spawn), no extra daemon needed.
|
|
8
|
+
// All transports (stdio, HTTP, SSE) go through the zero-dependency
|
|
9
|
+
// helper McpClient — no @modelcontextprotocol/sdk required.
|
|
10
|
+
|
|
11
|
+
export type HealthStatus = "healthy" | "degraded" | "down" | "unknown";
|
|
12
|
+
|
|
13
|
+
export interface ToolInfo {
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
inputSchema?: {
|
|
17
|
+
type?: string;
|
|
18
|
+
properties?: Record<string, {
|
|
19
|
+
type?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
default?: unknown;
|
|
22
|
+
enum?: string[];
|
|
23
|
+
items?: { type?: string };
|
|
24
|
+
}>;
|
|
25
|
+
required?: string[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HealthResult {
|
|
30
|
+
serverId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
status: HealthStatus;
|
|
33
|
+
latencyMs: number;
|
|
34
|
+
toolCount: number | null;
|
|
35
|
+
tools: string[] | null;
|
|
36
|
+
toolDetails?: ToolInfo[] | null;
|
|
37
|
+
instructions: string | null;
|
|
38
|
+
error: string | null;
|
|
39
|
+
checkedAt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ToolCallResult {
|
|
43
|
+
content: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
44
|
+
isError?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HealthCheckOptions {
|
|
48
|
+
timeoutMs?: number; // default 7000
|
|
49
|
+
includeTools?: boolean; // default true (list_tools)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type HelperClient = ReturnType<typeof McpClient.forStdio> | ReturnType<typeof McpClient.forHttp>;
|
|
53
|
+
|
|
54
|
+
// command is e.g. "npx -y some-mcp" stored as string — first token is the
|
|
55
|
+
// executable, the rest are args.
|
|
56
|
+
function splitCommand(command: string): { command: string; args: string[] } {
|
|
57
|
+
const parts = command.trim().split(/\s+/);
|
|
58
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dial(server: McpServer, timeoutMs: number): HelperClient | null {
|
|
62
|
+
const clientInfo = { name: "paseo-mcp-health", version: "1.0.0" };
|
|
63
|
+
if (server.url) {
|
|
64
|
+
return McpClient.forHttp(server.url, { timeoutMs, clientInfo });
|
|
65
|
+
}
|
|
66
|
+
if (server.command) {
|
|
67
|
+
// Prefer the structured args when the server definition carries
|
|
68
|
+
// them. Falling back to splitting the command string would drop
|
|
69
|
+
// them and spawn a bare runtime that hangs until timeout.
|
|
70
|
+
const { command, args } =
|
|
71
|
+
server.args && server.args.length > 0
|
|
72
|
+
? { command: server.command, args: server.args }
|
|
73
|
+
: splitCommand(server.command);
|
|
74
|
+
return McpClient.forStdio(command, args, undefined, { timeoutMs, clientInfo });
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readInstructions(client: HelperClient): string | null {
|
|
80
|
+
const raw = client.instructions;
|
|
81
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stderrOf(client: HelperClient): string | null {
|
|
85
|
+
if ("getStderr" in client && typeof client.getStderr === "function") {
|
|
86
|
+
try {
|
|
87
|
+
const text = (client.getStderr as () => string)();
|
|
88
|
+
return text?.trim() ? text : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toToolDetails(list: Array<{ name: string; description?: string; inputSchema?: unknown }>): {
|
|
97
|
+
tools: string[];
|
|
98
|
+
toolDetails: ToolInfo[];
|
|
99
|
+
} {
|
|
100
|
+
return {
|
|
101
|
+
tools: list.map((t) => t.name),
|
|
102
|
+
toolDetails: list.map((t) => ({
|
|
103
|
+
name: t.name,
|
|
104
|
+
description: t.description,
|
|
105
|
+
inputSchema: t.inputSchema as ToolInfo["inputSchema"],
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function checkMcpServerHealth(
|
|
111
|
+
server: McpServer,
|
|
112
|
+
opts: HealthCheckOptions = {},
|
|
113
|
+
): Promise<HealthResult> {
|
|
114
|
+
const timeoutMs = opts.timeoutMs ?? 7000;
|
|
115
|
+
const started = Date.now();
|
|
116
|
+
const checkedAt = new Date().toISOString();
|
|
117
|
+
|
|
118
|
+
const client = dial(server, timeoutMs);
|
|
119
|
+
if (!client) {
|
|
120
|
+
return {
|
|
121
|
+
serverId: server.id,
|
|
122
|
+
name: server.name,
|
|
123
|
+
status: "unknown",
|
|
124
|
+
latencyMs: 0,
|
|
125
|
+
toolCount: null,
|
|
126
|
+
tools: null,
|
|
127
|
+
instructions: null,
|
|
128
|
+
error: "No command or url to dial — unknown transport",
|
|
129
|
+
checkedAt,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const down = (base: string, includeStderr: boolean): HealthResult => {
|
|
134
|
+
const stderr = includeStderr ? stderrOf(client) : null;
|
|
135
|
+
return {
|
|
136
|
+
serverId: server.id,
|
|
137
|
+
name: server.name,
|
|
138
|
+
status: "down",
|
|
139
|
+
latencyMs: Date.now() - started,
|
|
140
|
+
toolCount: null,
|
|
141
|
+
tools: null,
|
|
142
|
+
toolDetails: null,
|
|
143
|
+
instructions: null,
|
|
144
|
+
error: stderr ? `${base}\nRecent stderr:\n${stderr}` : base,
|
|
145
|
+
checkedAt,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
if (opts.includeTools !== false) {
|
|
151
|
+
try {
|
|
152
|
+
const list = await withTimeout(client.listTools(), timeoutMs, "listTools");
|
|
153
|
+
const { tools, toolDetails } = toToolDetails(list);
|
|
154
|
+
const result: HealthResult = {
|
|
155
|
+
serverId: server.id,
|
|
156
|
+
name: server.name,
|
|
157
|
+
status: "healthy",
|
|
158
|
+
latencyMs: Date.now() - started,
|
|
159
|
+
toolCount: tools.length,
|
|
160
|
+
tools,
|
|
161
|
+
toolDetails,
|
|
162
|
+
instructions: readInstructions(client),
|
|
163
|
+
error: null,
|
|
164
|
+
checkedAt,
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
await client.close();
|
|
168
|
+
} catch {}
|
|
169
|
+
return result;
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// Connected but can't list tools = degraded. Verify reachability first.
|
|
172
|
+
try {
|
|
173
|
+
const ping = await withTimeout(client.ping(), timeoutMs, "ping");
|
|
174
|
+
if (ping.healthy) {
|
|
175
|
+
const result: HealthResult = {
|
|
176
|
+
serverId: server.id,
|
|
177
|
+
name: server.name,
|
|
178
|
+
status: "degraded",
|
|
179
|
+
latencyMs: Date.now() - started,
|
|
180
|
+
toolCount: null,
|
|
181
|
+
tools: null,
|
|
182
|
+
toolDetails: null,
|
|
183
|
+
instructions: readInstructions(client),
|
|
184
|
+
error: e instanceof Error ? e.message : String(e),
|
|
185
|
+
checkedAt,
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
await client.close();
|
|
189
|
+
} catch {}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
} catch {}
|
|
193
|
+
const result = down(e instanceof Error ? e.message : String(e), true);
|
|
194
|
+
try {
|
|
195
|
+
await client.close();
|
|
196
|
+
} catch {}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const ping = await withTimeout(client.ping(), timeoutMs, "ping");
|
|
202
|
+
const latencyMs = Date.now() - started;
|
|
203
|
+
const result: HealthResult = ping.healthy
|
|
204
|
+
? {
|
|
205
|
+
serverId: server.id,
|
|
206
|
+
name: server.name,
|
|
207
|
+
status: "healthy",
|
|
208
|
+
latencyMs,
|
|
209
|
+
toolCount: null,
|
|
210
|
+
tools: null,
|
|
211
|
+
toolDetails: null,
|
|
212
|
+
instructions: readInstructions(client),
|
|
213
|
+
error: null,
|
|
214
|
+
checkedAt,
|
|
215
|
+
}
|
|
216
|
+
: {
|
|
217
|
+
serverId: server.id,
|
|
218
|
+
name: server.name,
|
|
219
|
+
status: "down",
|
|
220
|
+
latencyMs,
|
|
221
|
+
toolCount: null,
|
|
222
|
+
tools: null,
|
|
223
|
+
toolDetails: null,
|
|
224
|
+
instructions: null,
|
|
225
|
+
error: ping.error ?? "ping failed",
|
|
226
|
+
checkedAt,
|
|
227
|
+
};
|
|
228
|
+
try {
|
|
229
|
+
await client.close();
|
|
230
|
+
} catch {}
|
|
231
|
+
return result;
|
|
232
|
+
} catch (e) {
|
|
233
|
+
const result = down(e instanceof Error ? e.message : String(e), true);
|
|
234
|
+
try {
|
|
235
|
+
await client.close();
|
|
236
|
+
} catch {}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function callMcpServerTool(
|
|
242
|
+
server: McpServer,
|
|
243
|
+
toolName: string,
|
|
244
|
+
args: Record<string, unknown> = {},
|
|
245
|
+
timeoutMs = 15000,
|
|
246
|
+
): Promise<ToolCallResult> {
|
|
247
|
+
const client = dial(server, timeoutMs);
|
|
248
|
+
if (!client) {
|
|
249
|
+
throw new Error(`Cannot execute tool on ${server.name}: unknown transport`);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const result = await withTimeout(client.callTool(toolName, args), timeoutMs, `callTool:${toolName}`);
|
|
253
|
+
return result as ToolCallResult;
|
|
254
|
+
} finally {
|
|
255
|
+
try {
|
|
256
|
+
await client.close();
|
|
257
|
+
} catch {}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Batch helper — parallel with concurrency cap so we don't fork-bomb the daemon.
|
|
262
|
+
export async function checkMany(
|
|
263
|
+
servers: McpServer[],
|
|
264
|
+
opts: HealthCheckOptions & { concurrency?: number } = {},
|
|
265
|
+
): Promise<HealthResult[]> {
|
|
266
|
+
const concurrency = opts.concurrency ?? 4;
|
|
267
|
+
const out: HealthResult[] = [];
|
|
268
|
+
for (let i = 0; i < servers.length; i += concurrency) {
|
|
269
|
+
const chunk = servers.slice(i, i + concurrency);
|
|
270
|
+
const results = await Promise.all(chunk.map((s) => checkMcpServerHealth(s, opts)));
|
|
271
|
+
out.push(...results);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
package/server/inject.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PluginBeforeRequests } from "@getpaseo/plugin/server";
|
|
2
|
+
|
|
3
|
+
export const GATEWAY_SERVER_NAME = "gateway";
|
|
4
|
+
|
|
5
|
+
export interface GatewayInjectOptions {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type CreateRequest = PluginBeforeRequests["agent.create"];
|
|
11
|
+
|
|
12
|
+
// Returns a modified agent.create request with the gateway MCP server
|
|
13
|
+
// injected, or undefined to keep the caller's request unchanged.
|
|
14
|
+
export function injectGatewayIntoCreateRequest(
|
|
15
|
+
request: CreateRequest,
|
|
16
|
+
opts: GatewayInjectOptions,
|
|
17
|
+
): CreateRequest | undefined {
|
|
18
|
+
if (!opts.enabled) return undefined;
|
|
19
|
+
if (!opts.url) return undefined;
|
|
20
|
+
const mcpServers = request.config.mcpServers ?? {};
|
|
21
|
+
if (mcpServers[GATEWAY_SERVER_NAME]) return undefined;
|
|
22
|
+
return {
|
|
23
|
+
...request,
|
|
24
|
+
config: {
|
|
25
|
+
...request.config,
|
|
26
|
+
mcpServers: {
|
|
27
|
+
...mcpServers,
|
|
28
|
+
[GATEWAY_SERVER_NAME]: { type: "http", url: opts.url },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
package/server/mcp.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
6
|
+
import type { McpServerSchema, McpStatusSnapshot, McpToolsSettings } from "../shared/mcp";
|
|
7
|
+
import { McpToolsSettingsSchema, buildHealthDigest } from "../shared/mcp";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { probeForProvider } from "./providers";
|
|
10
|
+
import { paseo as paseoProbe } from "./providers/catalog";
|
|
11
|
+
import { PLUGIN_VERSION } from "../shared/version";
|
|
12
|
+
// Direct source import. The helper MCP client has zero runtime deps,
|
|
13
|
+
// so no bundling step is needed for the daemon to resolve it.
|
|
14
|
+
import { createPluginLogger, PluginStorage, redactSecrets } from "paseo-plugin-helper/server";
|
|
15
|
+
import { checkMany, checkMcpServerHealth, callMcpServerTool } from "./health/health";
|
|
16
|
+
|
|
17
|
+
type McpServer = z.infer<typeof McpServerSchema>;
|
|
18
|
+
|
|
19
|
+
export const log = createPluginLogger("mcp-tools");
|
|
20
|
+
|
|
21
|
+
// Shared health snapshot for other plugins to consume without re-probing.
|
|
22
|
+
const statusStorage = new PluginStorage<McpStatusSnapshot>("mcp-tools", "status.json");
|
|
23
|
+
|
|
24
|
+
// Local settings reader. Kept separate from server/settings.ts to avoid
|
|
25
|
+
// a module cycle, since that module imports the logger from here.
|
|
26
|
+
const settingsReader = new PluginStorage<McpToolsSettings>("mcp-tools", "settings.json", {
|
|
27
|
+
schema: McpToolsSettingsSchema,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function healthDigestRowsEnabled(): boolean {
|
|
31
|
+
try {
|
|
32
|
+
return settingsReader.read().healthDigestRows === true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PASEO_TOOLS: Array<{ name: string; description: string; category: string }> = [
|
|
39
|
+
{ name: "create_agent", description: "Create an agent, optionally in a workspace", category: "Agents" },
|
|
40
|
+
{ name: "send_agent_prompt", description: "Send a task to a running agent", category: "Agents" },
|
|
41
|
+
{ name: "get_agent_status", description: "Return latest snapshot for an agent", category: "Agents" },
|
|
42
|
+
{ name: "list_agents", description: "List recent agents as compact metadata", category: "Agents" },
|
|
43
|
+
{ name: "archive_agent", description: "Soft-delete an agent", category: "Agents" },
|
|
44
|
+
{ name: "create_workspace", description: "Create a local or worktree-isolated workspace", category: "Workspaces" },
|
|
45
|
+
{ name: "list_workspaces", description: "List active workspaces", category: "Workspaces" },
|
|
46
|
+
{ name: "archive_workspace", description: "Archive a workspace and its sessions", category: "Workspaces" },
|
|
47
|
+
{ name: "list_workspace_scripts", description: "List configured workspace scripts", category: "Scripts" },
|
|
48
|
+
{ name: "start_workspace_script", description: "Start a configured script", category: "Scripts" },
|
|
49
|
+
{ name: "stop_workspace_script", description: "Stop a running script", category: "Scripts" },
|
|
50
|
+
{ name: "list_terminals", description: "List terminal sessions", category: "Terminals" },
|
|
51
|
+
{ name: "create_terminal", description: "Create a terminal session", category: "Terminals" },
|
|
52
|
+
{ name: "capture_terminal", description: "Capture terminal output", category: "Terminals" },
|
|
53
|
+
{ name: "send_terminal_keys", description: "Send keys to a terminal", category: "Terminals" },
|
|
54
|
+
{ name: "list_providers", description: "List configured agent providers", category: "Providers" },
|
|
55
|
+
{ name: "list_models", description: "List models for a provider", category: "Providers" },
|
|
56
|
+
{ name: "inspect_provider", description: "Inspect provider capabilities", category: "Providers" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function redact(text: string): string {
|
|
60
|
+
return redactSecrets(text, { mask: "•••" });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function redactConfig(def: unknown): string {
|
|
64
|
+
return JSON.stringify(redactSecrets(def, { mask: "•••" }), null, 2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadAgent(agentId: string, context: PluginHandlerContext) {
|
|
68
|
+
const handle = context.paseo.agents.ref(agentId);
|
|
69
|
+
const refreshed = await handle.refresh();
|
|
70
|
+
const agent = refreshed?.agent ?? handle.current();
|
|
71
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
72
|
+
const snapshot = agent as Record<string, unknown>;
|
|
73
|
+
const labelCandidates = ["providerLabel", "label", "displayName", "model"]
|
|
74
|
+
.map((k) => snapshot[k])
|
|
75
|
+
.filter((v): v is string => typeof v === "string" && v.length > 0);
|
|
76
|
+
return { provider: agent.provider, cwd: agent.cwd, handle, snapshot: agent, labelCandidates };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function findStoredRecord(agentId: string): Promise<Record<string, unknown> | null> {
|
|
80
|
+
const base = path.join(os.homedir(), ".paseo", "agents");
|
|
81
|
+
if (!existsSync(base)) return null;
|
|
82
|
+
try {
|
|
83
|
+
const projects = await readdir(base, { withFileTypes: true });
|
|
84
|
+
for (const ent of projects) {
|
|
85
|
+
const cand = ent.isDirectory()
|
|
86
|
+
? path.join(base, ent.name, `${agentId}.json`)
|
|
87
|
+
: path.join(base, ent.name);
|
|
88
|
+
if (ent.isFile() && ent.name !== `${agentId}.json`) continue;
|
|
89
|
+
if (existsSync(cand)) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(await readFile(cand, "utf8")) as Record<string, unknown>;
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Cache provider probe results keyed by `provider:cwd` to avoid redundant
|
|
100
|
+
// filesystem inspections and CLI process spawns when multiple agents share the same project/tool.
|
|
101
|
+
const PROBE_CACHE_TTL_MS = 60_000; // 1 minute TTL
|
|
102
|
+
const probeCache = new Map<string, { timestamp: number; result: { servers: McpServer[]; error: string | null } }>();
|
|
103
|
+
|
|
104
|
+
async function getCachedProviderProbe(agentId: string, provider: string, cwd: string, bypassCache = false, labels: string[] = []) {
|
|
105
|
+
const cacheKey = `${provider}:${cwd}`;
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const cached = probeCache.get(cacheKey);
|
|
108
|
+
|
|
109
|
+
if (!bypassCache && cached && now - cached.timestamp < PROBE_CACHE_TTL_MS) {
|
|
110
|
+
return cached.result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const probe = probeForProvider(provider, ...labels);
|
|
114
|
+
if (!probe) {
|
|
115
|
+
return { servers: [], error: null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const res = await probe.probe({ agentId, provider, cwd });
|
|
119
|
+
const result = { servers: res.servers, error: res.error ?? null };
|
|
120
|
+
probeCache.set(cacheKey, { timestamp: now, result });
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Live servers for THIS agent only.
|
|
126
|
+
* 1) Paseo-injected `mcpServers` from StoredAgentRecord (session-scoped custom servers)
|
|
127
|
+
* 2) Provider-specific live probe — cached by `provider:cwd` across concurrent agents
|
|
128
|
+
*/
|
|
129
|
+
export async function discoverLiveServers(
|
|
130
|
+
agentId: string,
|
|
131
|
+
context: PluginHandlerContext,
|
|
132
|
+
options: { bypassCache?: boolean } = {},
|
|
133
|
+
): Promise<{ servers: McpServer[]; error: string | null }> {
|
|
134
|
+
const agent = await loadAgent(agentId, context);
|
|
135
|
+
const servers: McpServer[] = [];
|
|
136
|
+
let error: string | null = null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const paseoRes = await paseoProbe.probe({ agentId, provider: "paseo", cwd: agent.cwd });
|
|
140
|
+
for (const s of paseoRes.servers) {
|
|
141
|
+
if (!servers.some((existing) => existing.id === s.id || existing.name === s.name)) {
|
|
142
|
+
servers.push(s);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const record = await findStoredRecord(agentId);
|
|
147
|
+
const cfg = (record?.config ?? {}) as Record<string, unknown>;
|
|
148
|
+
const mcpServers = (cfg.mcpServers ?? {}) as Record<string, unknown>;
|
|
149
|
+
for (const [name, defRaw] of Object.entries(mcpServers)) {
|
|
150
|
+
if (!defRaw || typeof defRaw !== "object" || name === "paseo") continue;
|
|
151
|
+
const def = defRaw as Record<string, unknown>;
|
|
152
|
+
const url = typeof def.url === "string" ? def.url : null;
|
|
153
|
+
const command = typeof def.command === "string" ? def.command : null;
|
|
154
|
+
const defArgs = Array.isArray(def.args) ? def.args.filter((a) => typeof a === "string") : [];
|
|
155
|
+
servers.push({
|
|
156
|
+
id: `session:paseo:${name}`,
|
|
157
|
+
name,
|
|
158
|
+
transport: url ? "http" : command ? "stdio" : "unknown",
|
|
159
|
+
source: { kind: "session", label: `session · ${agent.provider}`, path: `agent:${agentId}` },
|
|
160
|
+
command,
|
|
161
|
+
args: defArgs.length > 0 ? defArgs : null,
|
|
162
|
+
url,
|
|
163
|
+
description: url ?? command ?? JSON.stringify(def).slice(0, 80),
|
|
164
|
+
hasSecrets: Boolean(def.env || def.headers),
|
|
165
|
+
configPreview: redactConfig(def),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const probeResult = await getCachedProviderProbe(agentId, agent.provider, agent.cwd, options.bypassCache, agent.labelCandidates);
|
|
170
|
+
for (const s of probeResult.servers) {
|
|
171
|
+
if (servers.some((existing) => existing.name === s.name)) continue;
|
|
172
|
+
servers.push(s);
|
|
173
|
+
}
|
|
174
|
+
if (probeResult.error) {
|
|
175
|
+
error = probeResult.error;
|
|
176
|
+
log.warn("Provider probe reported error", { provider: agent.provider, error });
|
|
177
|
+
}
|
|
178
|
+
log.info("Discovered live servers", { provider: agent.provider, count: servers.length });
|
|
179
|
+
} catch (e) {
|
|
180
|
+
error = e instanceof Error ? e.message : String(e);
|
|
181
|
+
log.error("Failed to discover live servers", { provider: agent.provider, error });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { servers, error };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createListMcpHandler() {
|
|
188
|
+
return async (input: { agentId: string }, context: PluginHandlerContext) => {
|
|
189
|
+
const agent = await loadAgent(input.agentId, context);
|
|
190
|
+
const { servers, error } = await discoverLiveServers(input.agentId, context);
|
|
191
|
+
return { provider: agent.provider, cwd: agent.cwd, servers, paseoTools: PASEO_TOOLS, error };
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createReadMcpHandler() {
|
|
196
|
+
return async (input: { agentId: string; serverId: string }, context: PluginHandlerContext) => {
|
|
197
|
+
const { servers } = await discoverLiveServers(input.agentId, context);
|
|
198
|
+
const entry = servers.find((s) => s.id === input.serverId);
|
|
199
|
+
if (!entry) throw new Error(`MCP server not found in this session: ${input.serverId}`);
|
|
200
|
+
return {
|
|
201
|
+
name: entry.name,
|
|
202
|
+
transport: entry.transport,
|
|
203
|
+
source: entry.source,
|
|
204
|
+
path: entry.source.path,
|
|
205
|
+
raw: entry.configPreview,
|
|
206
|
+
redacted: entry.configPreview,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createHealthHandler() {
|
|
212
|
+
return async (input: { agentId: string; serverId?: string }, context: PluginHandlerContext) => {
|
|
213
|
+
const { servers, error } = await discoverLiveServers(input.agentId, context);
|
|
214
|
+
const targets = input.serverId ? servers.filter((s) => s.id === input.serverId) : servers;
|
|
215
|
+
if (input.serverId && targets.length === 0) {
|
|
216
|
+
throw new Error(`MCP server not found in this session: ${input.serverId}`);
|
|
217
|
+
}
|
|
218
|
+
const results = await checkMany(targets, { timeoutMs: 7000, includeTools: true });
|
|
219
|
+
const snapshot: McpStatusSnapshot = {
|
|
220
|
+
updatedAt: new Date().toISOString(),
|
|
221
|
+
total: results.length,
|
|
222
|
+
healthy: results.filter((r) => r.status === "healthy").length,
|
|
223
|
+
degraded: results.filter((r) => r.status === "degraded").length,
|
|
224
|
+
down: results.filter((r) => r.status === "down").length,
|
|
225
|
+
servers: results.map((r) => ({ name: r.name, status: r.status, latencyMs: r.latencyMs })),
|
|
226
|
+
};
|
|
227
|
+
await statusStorage.writeAsync(snapshot).catch((e) => {
|
|
228
|
+
log.warn("Failed to persist status snapshot", { error: e instanceof Error ? e.message : String(e) });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (healthDigestRowsEnabled()) {
|
|
232
|
+
await context.paseo.agents
|
|
233
|
+
.ref(input.agentId)
|
|
234
|
+
.timeline.append({
|
|
235
|
+
type: "plugin",
|
|
236
|
+
id: "mcp-health",
|
|
237
|
+
kind: "mcp-health-digest",
|
|
238
|
+
version: 1,
|
|
239
|
+
data: buildHealthDigest(snapshot),
|
|
240
|
+
})
|
|
241
|
+
.catch((e) => {
|
|
242
|
+
log.warn("Failed to append health digest row", { error: e instanceof Error ? e.message : String(e) });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { results, error };
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createCallMcpToolHandler() {
|
|
251
|
+
return async (
|
|
252
|
+
input: { agentId: string; serverId: string; toolName: string; arguments: Record<string, unknown> },
|
|
253
|
+
context: PluginHandlerContext,
|
|
254
|
+
) => {
|
|
255
|
+
const { servers } = await discoverLiveServers(input.agentId, context);
|
|
256
|
+
const server = servers.find((s) => s.id === input.serverId);
|
|
257
|
+
if (!server) {
|
|
258
|
+
throw new Error(`MCP server not found in this session: ${input.serverId}`);
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const result = await (callMcpServerTool as (s: McpServer, t: string, a: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text?: string; [key: string]: unknown }>; isError?: boolean }>)(
|
|
262
|
+
server,
|
|
263
|
+
input.toolName,
|
|
264
|
+
input.arguments,
|
|
265
|
+
);
|
|
266
|
+
return {
|
|
267
|
+
content: result.content ?? [],
|
|
268
|
+
isError: result.isError,
|
|
269
|
+
error: null,
|
|
270
|
+
};
|
|
271
|
+
} catch (e) {
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
274
|
+
isError: true,
|
|
275
|
+
error: e instanceof Error ? e.message : String(e),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function createDiagnoseMcpHandler() {
|
|
282
|
+
return async (input: { agentId: string }, context: PluginHandlerContext) => {
|
|
283
|
+
const agent = await loadAgent(input.agentId, context);
|
|
284
|
+
const steps: Array<{ target: string; status: "found" | "missing" | "error" | "skipped"; details: string; contentPreview: string | null }> = [];
|
|
285
|
+
const home = os.homedir();
|
|
286
|
+
|
|
287
|
+
// 1. Check Paseo stored agent record
|
|
288
|
+
try {
|
|
289
|
+
const record = await findStoredRecord(input.agentId);
|
|
290
|
+
if (record) {
|
|
291
|
+
const cfg = (record.config ?? {}) as Record<string, unknown>;
|
|
292
|
+
const mcpServers = (cfg.mcpServers ?? {}) as Record<string, unknown>;
|
|
293
|
+
const names = Object.keys(mcpServers);
|
|
294
|
+
steps.push({
|
|
295
|
+
target: `Paseo Agent Record (${input.agentId})`,
|
|
296
|
+
status: "found",
|
|
297
|
+
details: `Found record with ${names.length} MCP server(s): ${names.join(", ") || "none"}`,
|
|
298
|
+
contentPreview: redactConfig(cfg.mcpServers ?? {}),
|
|
299
|
+
});
|
|
300
|
+
} else {
|
|
301
|
+
steps.push({
|
|
302
|
+
target: `Paseo Agent Record (${input.agentId})`,
|
|
303
|
+
status: "missing",
|
|
304
|
+
details: "No stored record file found in ~/.paseo/agents",
|
|
305
|
+
contentPreview: null,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
} catch (e) {
|
|
309
|
+
steps.push({
|
|
310
|
+
target: `Paseo Agent Record (${input.agentId})`,
|
|
311
|
+
status: "error",
|
|
312
|
+
details: e instanceof Error ? e.message : String(e),
|
|
313
|
+
contentPreview: null,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 2. Run Provider Probe & Collect Diagnostic Steps Polymorphically
|
|
318
|
+
const probe = probeForProvider(agent.provider, ...agent.labelCandidates);
|
|
319
|
+
let discoveredServerCount = 0;
|
|
320
|
+
let probeError: string | null = null;
|
|
321
|
+
|
|
322
|
+
if (probe) {
|
|
323
|
+
try {
|
|
324
|
+
const result = await probe.probe({
|
|
325
|
+
agentId: input.agentId,
|
|
326
|
+
provider: agent.provider,
|
|
327
|
+
cwd: agent.cwd,
|
|
328
|
+
sessionId: (agent as Record<string, unknown>).sessionId as string | undefined,
|
|
329
|
+
});
|
|
330
|
+
discoveredServerCount = result.servers.length;
|
|
331
|
+
if (result.error) probeError = result.error;
|
|
332
|
+
if (result.steps && result.steps.length > 0) {
|
|
333
|
+
steps.push(...result.steps);
|
|
334
|
+
}
|
|
335
|
+
} catch (e) {
|
|
336
|
+
probeError = e instanceof Error ? e.message : String(e);
|
|
337
|
+
steps.push({
|
|
338
|
+
target: `Provider Probe (${probe.id})`,
|
|
339
|
+
status: "error",
|
|
340
|
+
details: probeError,
|
|
341
|
+
contentPreview: null,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
steps.push({
|
|
346
|
+
target: `Provider Probe (${agent.provider})`,
|
|
347
|
+
status: "missing",
|
|
348
|
+
details: "No probe found matching this provider in registry",
|
|
349
|
+
contentPreview: null,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
report: "Paseo Provider MCP Probe Diagnostic",
|
|
355
|
+
version: PLUGIN_VERSION,
|
|
356
|
+
provider: agent.provider,
|
|
357
|
+
cwd: agent.cwd,
|
|
358
|
+
probeId: probe?.id ?? null,
|
|
359
|
+
probeLabel: probe?.label ?? null,
|
|
360
|
+
steps,
|
|
361
|
+
discoveredServerCount,
|
|
362
|
+
error: probeError,
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
}
|