@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 command is installed before proceeding
121
- if (config.command) {
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
- connectWithHandshakeTimeout(client: Client, transport: SSEClientTransport | StdioClientTransport, ms?: number): Promise<{
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
- async connectWithHandshakeTimeout(client, transport, ms = 60000) {
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
- client.connect(transport),
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
- throw err;
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.25";
1
+ export declare const version = "0.1.26";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '0.1.25';
1
+ export const version = '0.1.26';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolplex/client",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "author": "ToolPlex LLC",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "description": "The official ToolPlex client for AI agent tool discovery and execution",