@toolplex/client 0.1.25 → 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.
|
@@ -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
|
|
@@ -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;
|
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';
|