@toolplex/client 0.1.24 → 0.1.26
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/dist/mcp-server/toolHandlers/installServerHandler.js +18 -2
- package/dist/mcp-server/toolHandlers/savePlaybookHandler.js +35 -16
- package/dist/server-manager/serverManager.d.ts +6 -1
- package/dist/server-manager/serverManager.js +56 -4
- package/dist/shared/mcpServerTypes.d.ts +3 -0
- package/dist/shared/mcpServerTypes.js +3 -0
- package/dist/src/shared/mcpServerTypes.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -117,8 +117,24 @@ export async function handleInstallServer(params) {
|
|
|
117
117
|
}
|
|
118
118
|
// Validate server ID format
|
|
119
119
|
validateServerIdOrThrow(server_id);
|
|
120
|
-
// Validate
|
|
121
|
-
if (config.
|
|
120
|
+
// Validate stdio transport configuration early to avoid timeouts
|
|
121
|
+
if (config.transport === "stdio") {
|
|
122
|
+
// Validate that command is provided
|
|
123
|
+
if (!config.command) {
|
|
124
|
+
throw new Error("Command is required for stdio transport");
|
|
125
|
+
}
|
|
126
|
+
// Validate command is installed
|
|
127
|
+
await RuntimeCheck.validateCommandOrThrow(config.command);
|
|
128
|
+
// Check that args is provided and not empty for package managers
|
|
129
|
+
// Package managers like npx, uvx, pnpm dlx, etc. require a package name as first arg
|
|
130
|
+
const command = config.command.toLowerCase();
|
|
131
|
+
const requiresPackageName = ["npx", "uvx", "pnpm", "yarn"].some((pm) => command.includes(pm));
|
|
132
|
+
if (requiresPackageName && (!config.args || config.args.length === 0)) {
|
|
133
|
+
throw new Error(`Package manager command '${config.command}' requires args to specify package name. Received args: ${config.args ? "[]" : "undefined"}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (config.command) {
|
|
137
|
+
// For non-stdio transports, still validate command if provided
|
|
122
138
|
await RuntimeCheck.validateCommandOrThrow(config.command);
|
|
123
139
|
}
|
|
124
140
|
// Check if server is disallowed using policy enforcer
|
|
@@ -3,7 +3,8 @@ import Registry from "../registry.js";
|
|
|
3
3
|
const logger = FileLogger;
|
|
4
4
|
export async function handleSavePlaybook(params) {
|
|
5
5
|
const startTime = Date.now();
|
|
6
|
-
|
|
6
|
+
const isValidationOnly = params.validate_only === true;
|
|
7
|
+
await logger.info(`Handling save playbook request${isValidationOnly ? " (validation only)" : ""}`);
|
|
7
8
|
await logger.debug(`Playbook params: ${JSON.stringify(params)}`);
|
|
8
9
|
const apiService = Registry.getToolplexApiService();
|
|
9
10
|
const telemetryLogger = Registry.getTelemetryLogger();
|
|
@@ -19,19 +20,34 @@ export async function handleSavePlaybook(params) {
|
|
|
19
20
|
if (clientContext.permissions.enable_read_only_mode) {
|
|
20
21
|
throw new Error("Saving playbooks is disabled in read-only mode");
|
|
21
22
|
}
|
|
22
|
-
// Enforce playbook policy before saving
|
|
23
|
+
// Enforce playbook policy before saving (runs in both validation and actual save)
|
|
23
24
|
policyEnforcer.enforceSavePlaybookPolicy(params);
|
|
25
|
+
// If validation-only mode, return success without saving or logging telemetry
|
|
26
|
+
if (isValidationOnly) {
|
|
27
|
+
await logger.info("Playbook validation passed");
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: JSON.stringify({ validation: "passed" }),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
24
37
|
const { playbook_name, description, actions, domain, keywords, requirements, privacy, source_playbook_id, fork_reason, } = params;
|
|
25
38
|
const response = await apiService.createPlaybook(playbook_name, description, actions, domain, keywords, requirements, privacy, source_playbook_id, fork_reason);
|
|
26
39
|
await logger.info(`Playbook created successfully with ID: ${response.id}`);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
// Only log telemetry for actual saves (not validation-only calls)
|
|
41
|
+
if (!isValidationOnly) {
|
|
42
|
+
await telemetryLogger.log("client_save_playbook", {
|
|
43
|
+
success: true,
|
|
44
|
+
log_context: {
|
|
45
|
+
playbook_id: response.id,
|
|
46
|
+
source_playbook_id: source_playbook_id,
|
|
47
|
+
},
|
|
48
|
+
latency_ms: Date.now() - startTime,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
35
51
|
return {
|
|
36
52
|
content: [
|
|
37
53
|
{
|
|
@@ -45,12 +61,15 @@ export async function handleSavePlaybook(params) {
|
|
|
45
61
|
}
|
|
46
62
|
catch (error) {
|
|
47
63
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
48
|
-
await logger.error(`Failed to create playbook: ${errorMessage}`);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
await logger.error(`Failed to ${isValidationOnly ? "validate" : "create"} playbook: ${errorMessage}`);
|
|
65
|
+
// Only log telemetry for actual saves (not validation-only calls)
|
|
66
|
+
if (!isValidationOnly) {
|
|
67
|
+
await telemetryLogger.log("client_save_playbook", {
|
|
68
|
+
success: false,
|
|
69
|
+
pii_sanitized_error_message: errorMessage,
|
|
70
|
+
latency_ms: Date.now() - startTime,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
54
73
|
return {
|
|
55
74
|
isError: true,
|
|
56
75
|
content: [
|
|
@@ -12,12 +12,17 @@ export declare class ServerManager {
|
|
|
12
12
|
private config;
|
|
13
13
|
private installationPromises;
|
|
14
14
|
private configLock;
|
|
15
|
+
private static readonly MAX_STDERR_LINES;
|
|
15
16
|
constructor();
|
|
16
17
|
private loadConfig;
|
|
17
18
|
private saveConfig;
|
|
18
19
|
initialize(): Promise<InitializeResult>;
|
|
19
20
|
getServerName(serverId: string): Promise<string>;
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Helper to attach stderr listener as soon as transport starts
|
|
23
|
+
*/
|
|
24
|
+
private attachStderrListener;
|
|
25
|
+
connectWithHandshakeTimeout(client: Client, transport: SSEClientTransport | StdioClientTransport, ms?: number, stderrBuffer?: string[], serverId?: string): Promise<{
|
|
21
26
|
tools?: Tool[];
|
|
22
27
|
}>;
|
|
23
28
|
install(serverId: string, serverName: string, description: string, config: ServerConfig): Promise<void>;
|
|
@@ -136,13 +136,51 @@ export class ServerManager {
|
|
|
136
136
|
await logger.debug(`Getting name for server ${serverId}`);
|
|
137
137
|
return this.serverNames.get(serverId) || serverId;
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Helper to attach stderr listener as soon as transport starts
|
|
141
|
+
*/
|
|
142
|
+
async attachStderrListener(transport, serverId, stderrBuffer, maxLines) {
|
|
143
|
+
// Poll for stderr availability (it becomes available after transport.start())
|
|
144
|
+
const maxAttempts = 100; // 1 second total
|
|
145
|
+
const pollInterval = 10; // 10ms between checks
|
|
146
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
147
|
+
if (transport.stderr) {
|
|
148
|
+
transport.stderr.on("data", (chunk) => {
|
|
149
|
+
const lines = chunk
|
|
150
|
+
.toString()
|
|
151
|
+
.split("\n")
|
|
152
|
+
.filter((l) => l.trim());
|
|
153
|
+
stderrBuffer.push(...lines);
|
|
154
|
+
// Keep only the last maxLines to prevent memory issues
|
|
155
|
+
if (stderrBuffer.length > maxLines) {
|
|
156
|
+
stderrBuffer.splice(0, stderrBuffer.length - maxLines);
|
|
157
|
+
}
|
|
158
|
+
// Also log stderr in real-time for debugging
|
|
159
|
+
lines.forEach((line) => {
|
|
160
|
+
logger.debug(`[${serverId} stderr] ${line}`);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
166
|
+
}
|
|
167
|
+
// If stderr never became available, that's okay (might be SSE transport)
|
|
168
|
+
}
|
|
169
|
+
async connectWithHandshakeTimeout(client, transport, ms = 60000, stderrBuffer, serverId) {
|
|
140
170
|
let connectTimeout;
|
|
141
171
|
let listToolsTimeout;
|
|
142
172
|
try {
|
|
173
|
+
// Start stderr monitoring in parallel for stdio transports
|
|
174
|
+
const stderrMonitoring = transport instanceof StdioClientTransport && stderrBuffer && serverId
|
|
175
|
+
? this.attachStderrListener(transport, serverId, stderrBuffer, ServerManager.MAX_STDERR_LINES)
|
|
176
|
+
: Promise.resolve();
|
|
143
177
|
// Race connect() with timeout
|
|
144
178
|
await Promise.race([
|
|
145
|
-
|
|
179
|
+
(async () => {
|
|
180
|
+
await client.connect(transport);
|
|
181
|
+
// Ensure stderr listener is attached after connection starts
|
|
182
|
+
await stderrMonitoring;
|
|
183
|
+
})(),
|
|
146
184
|
new Promise((_, reject) => {
|
|
147
185
|
connectTimeout = setTimeout(() => reject(new Error(`connect() timed out in ${ms} ms`)), ms);
|
|
148
186
|
}),
|
|
@@ -195,6 +233,7 @@ export class ServerManager {
|
|
|
195
233
|
await this.removeServer(serverId);
|
|
196
234
|
}
|
|
197
235
|
let transport;
|
|
236
|
+
const stderrBuffer = [];
|
|
198
237
|
if (config.transport === "sse") {
|
|
199
238
|
if (!config.url)
|
|
200
239
|
throw new Error("URL is required for SSE transport");
|
|
@@ -226,7 +265,7 @@ export class ServerManager {
|
|
|
226
265
|
}
|
|
227
266
|
const client = new Client({ name: serverId, version: "1.0.0" }, { capabilities: { prompts: {}, resources: {}, tools: {} } });
|
|
228
267
|
try {
|
|
229
|
-
const toolsResponse = await this.connectWithHandshakeTimeout(client, transport, 60000);
|
|
268
|
+
const toolsResponse = await this.connectWithHandshakeTimeout(client, transport, 60000, stderrBuffer, serverId);
|
|
230
269
|
const tools = toolsResponse.tools || [];
|
|
231
270
|
this.sessions.set(serverId, client);
|
|
232
271
|
this.tools.set(serverId, tools);
|
|
@@ -258,7 +297,18 @@ export class ServerManager {
|
|
|
258
297
|
await logger.warn(`Failed to close transport during cleanup: ${closeErr}`);
|
|
259
298
|
}
|
|
260
299
|
}
|
|
261
|
-
|
|
300
|
+
// Enhance error message with stderr output if available
|
|
301
|
+
const baseError = err instanceof Error ? err.message : String(err);
|
|
302
|
+
let enhancedError = baseError;
|
|
303
|
+
if (stderrBuffer.length > 0) {
|
|
304
|
+
const stderrPreview = stderrBuffer.join("\n");
|
|
305
|
+
enhancedError = `${baseError}\n\nServer stderr output:\n${stderrPreview}`;
|
|
306
|
+
await logger.error(`Installation failed for ${serverId}. Error: ${baseError}. Stderr: ${stderrPreview}`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
await logger.error(`Installation failed for ${serverId}: ${baseError}`);
|
|
310
|
+
}
|
|
311
|
+
throw new Error(enhancedError);
|
|
262
312
|
}
|
|
263
313
|
}
|
|
264
314
|
async callTool(serverId, toolName,
|
|
@@ -411,3 +461,5 @@ export class ServerManager {
|
|
|
411
461
|
this.installationPromises.clear();
|
|
412
462
|
}
|
|
413
463
|
}
|
|
464
|
+
// Maximum number of stderr lines to capture during installation
|
|
465
|
+
ServerManager.MAX_STDERR_LINES = 50;
|
|
@@ -316,6 +316,7 @@ export declare const SavePlaybookParamsSchema: z.ZodObject<{
|
|
|
316
316
|
privacy: z.ZodOptional<z.ZodEnum<["public", "private"]>>;
|
|
317
317
|
source_playbook_id: z.ZodOptional<z.ZodString>;
|
|
318
318
|
fork_reason: z.ZodOptional<z.ZodString>;
|
|
319
|
+
validate_only: z.ZodOptional<z.ZodBoolean>;
|
|
319
320
|
}, "strip", z.ZodTypeAny, {
|
|
320
321
|
description: string;
|
|
321
322
|
playbook_name: string;
|
|
@@ -333,6 +334,7 @@ export declare const SavePlaybookParamsSchema: z.ZodObject<{
|
|
|
333
334
|
privacy?: "public" | "private" | undefined;
|
|
334
335
|
source_playbook_id?: string | undefined;
|
|
335
336
|
fork_reason?: string | undefined;
|
|
337
|
+
validate_only?: boolean | undefined;
|
|
336
338
|
}, {
|
|
337
339
|
description: string;
|
|
338
340
|
playbook_name: string;
|
|
@@ -350,6 +352,7 @@ export declare const SavePlaybookParamsSchema: z.ZodObject<{
|
|
|
350
352
|
privacy?: "public" | "private" | undefined;
|
|
351
353
|
source_playbook_id?: string | undefined;
|
|
352
354
|
fork_reason?: string | undefined;
|
|
355
|
+
validate_only?: boolean | undefined;
|
|
353
356
|
}>;
|
|
354
357
|
export type SavePlaybookParams = z.infer<typeof SavePlaybookParamsSchema>;
|
|
355
358
|
export declare const LogPlaybookUsageParamsSchema: z.ZodObject<{
|
|
@@ -126,6 +126,9 @@ export const SavePlaybookParamsSchema = z.object({
|
|
|
126
126
|
privacy: z.enum(["public", "private"]).optional(),
|
|
127
127
|
source_playbook_id: z.string().optional(),
|
|
128
128
|
fork_reason: z.string().optional(),
|
|
129
|
+
// Internal parameter for validation-only mode (not exposed to agent in tool definition)
|
|
130
|
+
// Use coerce to handle both boolean and string "true"/"false" from different LLM clients
|
|
131
|
+
validate_only: z.coerce.boolean().optional(),
|
|
129
132
|
});
|
|
130
133
|
// --------------------
|
|
131
134
|
// LogPlaybookUsageParams
|
|
@@ -126,6 +126,9 @@ export const SavePlaybookParamsSchema = z.object({
|
|
|
126
126
|
privacy: z.enum(["public", "private"]).optional(),
|
|
127
127
|
source_playbook_id: z.string().optional(),
|
|
128
128
|
fork_reason: z.string().optional(),
|
|
129
|
+
// Internal parameter for validation-only mode (not exposed to agent in tool definition)
|
|
130
|
+
// Use coerce to handle both boolean and string "true"/"false" from different LLM clients
|
|
131
|
+
validate_only: z.coerce.boolean().optional(),
|
|
129
132
|
});
|
|
130
133
|
// --------------------
|
|
131
134
|
// LogPlaybookUsageParams
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.1.
|
|
1
|
+
export declare const version = "0.1.26";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.1.
|
|
1
|
+
export const version = '0.1.26';
|