@toolplex/client 0.1.25 → 0.1.27

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.
@@ -20,6 +20,22 @@ const bundledDependencies = {
20
20
  uvx: process.env.TOOLPLEX_UVX_PATH,
21
21
  git: process.env.TOOLPLEX_GIT_PATH,
22
22
  };
23
+ // Parse session resume history for restored chat sessions
24
+ // This allows the enforcement layer to validate save_playbook and submit_feedback
25
+ // based on historical tool usage from the database
26
+ let sessionResumeHistory;
27
+ if (process.env.TOOLPLEX_SESSION_RESUME_HISTORY) {
28
+ try {
29
+ sessionResumeHistory = JSON.parse(process.env.TOOLPLEX_SESSION_RESUME_HISTORY);
30
+ FileLogger.info(`Parsed session resume history - ` +
31
+ `${sessionResumeHistory?.tool_calls.length || 0} tool calls, ` +
32
+ `${sessionResumeHistory?.installs.length || 0} installs, ` +
33
+ `${sessionResumeHistory?.uninstalls.length || 0} uninstalls`);
34
+ }
35
+ catch (error) {
36
+ FileLogger.warn(`Failed to parse session resume history: ${error}`);
37
+ }
38
+ }
23
39
  if (!apiKey) {
24
40
  process.exit(1);
25
41
  }
@@ -30,6 +46,7 @@ const config = {
30
46
  clientName,
31
47
  logLevel,
32
48
  bundledDependencies,
49
+ sessionResumeHistory,
33
50
  };
34
51
  serve(config).catch(() => {
35
52
  process.exit(1);
@@ -2,6 +2,10 @@ declare class CallToolObserver {
2
2
  private serverToolCalls;
3
3
  constructor();
4
4
  recordCall(serverId: string, toolName: string): void;
5
+ seedHistory(history: Array<{
6
+ server_id: string;
7
+ tool_name: string;
8
+ }>): void;
5
9
  wasServerCalled(serverId: string): boolean;
6
10
  wasToolCalled(serverId: string, toolName: string): boolean;
7
11
  clear(): void;
@@ -9,6 +9,12 @@ class CallToolObserver {
9
9
  }
10
10
  this.serverToolCalls.get(serverId).add(toolName);
11
11
  }
12
+ // Seed the observer with historical tool calls (for session resume)
13
+ seedHistory(history) {
14
+ history.forEach(({ server_id, tool_name }) => {
15
+ this.recordCall(server_id, tool_name);
16
+ });
17
+ }
12
18
  // Check if a server was called at all
13
19
  wasServerCalled(serverId) {
14
20
  return (this.serverToolCalls.has(serverId) &&
@@ -3,6 +3,11 @@ declare class InstallObserver {
3
3
  constructor();
4
4
  recordInstall(serverId: string): void;
5
5
  recordUninstall(serverId: string): void;
6
+ seedHistory(installs: Array<{
7
+ server_id: string;
8
+ }>, uninstalls: Array<{
9
+ server_id: string;
10
+ }>): void;
6
11
  wasServerInstalled(serverId: string): boolean;
7
12
  wasServerUninstalled(serverId: string): boolean;
8
13
  clear(): void;
@@ -10,6 +10,15 @@ class InstallObserver {
10
10
  recordUninstall(serverId) {
11
11
  this.recordAction(serverId, "uninstall");
12
12
  }
13
+ // Seed the observer with historical install/uninstall actions (for session resume)
14
+ seedHistory(installs, uninstalls) {
15
+ installs.forEach(({ server_id }) => {
16
+ this.recordInstall(server_id);
17
+ });
18
+ uninstalls.forEach(({ server_id }) => {
19
+ this.recordUninstall(server_id);
20
+ });
21
+ }
13
22
  // Check if a server has been installed
14
23
  wasServerInstalled(serverId) {
15
24
  return (this.serverInstallActions.has(serverId) &&
@@ -6,7 +6,7 @@ import { PromptsCache } from "./promptsCache.js";
6
6
  import { ToolDefinitionsCache } from "./toolDefinitionsCache.js";
7
7
  import { ServersCache } from "./serversCache.js";
8
8
  import { PolicyEnforcer } from "./policy/policyEnforcer.js";
9
- import { BundledDependencies } from "../shared/mcpServerTypes.js";
9
+ import { BundledDependencies, ToolplexServerConfig } from "../shared/mcpServerTypes.js";
10
10
  /**
11
11
  * In-memory global registry for the ToolPlex client.
12
12
  * Maintains singleton instances of core services and clients used throughout the application.
@@ -21,6 +21,7 @@ declare class Registry {
21
21
  private static _serversCache;
22
22
  private static _policyEnforcer;
23
23
  private static _bundledDependencies;
24
+ private static _serverConfig;
24
25
  static init(clientContext: ClientContext): Promise<void>;
25
26
  static getClientContext(): ClientContext;
26
27
  static getToolplexApiService(): ToolplexApiService;
@@ -46,6 +47,14 @@ declare class Registry {
46
47
  * Returns undefined if the dependency is not available.
47
48
  */
48
49
  static getBundledDependencyPath(depName: "node" | "python" | "git" | "uvx" | "npx"): string | undefined;
50
+ /**
51
+ * Set the server configuration (includes session resume history).
52
+ */
53
+ static setServerConfig(config: ToolplexServerConfig): void;
54
+ /**
55
+ * Get the server configuration.
56
+ */
57
+ static getServerConfig(): ToolplexServerConfig;
49
58
  static reset(): void;
50
59
  }
51
60
  export default Registry;
@@ -101,6 +101,21 @@ class Registry {
101
101
  static getBundledDependencyPath(depName) {
102
102
  return this._bundledDependencies[depName];
103
103
  }
104
+ /**
105
+ * Set the server configuration (includes session resume history).
106
+ */
107
+ static setServerConfig(config) {
108
+ this._serverConfig = config;
109
+ }
110
+ /**
111
+ * Get the server configuration.
112
+ */
113
+ static getServerConfig() {
114
+ if (!this._serverConfig) {
115
+ throw new Error("ServerConfig not set in Registry");
116
+ }
117
+ return this._serverConfig;
118
+ }
104
119
  static reset() {
105
120
  this._clientContext = null;
106
121
  this._toolplexApiService = null;
@@ -120,6 +135,7 @@ class Registry {
120
135
  }
121
136
  this._policyEnforcer = null;
122
137
  this._bundledDependencies = {};
138
+ this._serverConfig = null;
123
139
  }
124
140
  }
125
141
  Registry._clientContext = null;
@@ -131,4 +147,5 @@ Registry._toolDefinitionsCache = null;
131
147
  Registry._serversCache = null;
132
148
  Registry._policyEnforcer = null;
133
149
  Registry._bundledDependencies = {};
150
+ Registry._serverConfig = null;
134
151
  export default Registry;
@@ -100,6 +100,21 @@ export async function handleInitialize(params) {
100
100
  promptsCache.init(processedPrompts);
101
101
  // Init PolicyEnforce after setting permissions and flags
102
102
  policyEnforcer.init(clientContext);
103
+ // Seed enforcement history if provided (for restored sessions)
104
+ const serverConfig = Registry.getServerConfig();
105
+ if (serverConfig.sessionResumeHistory) {
106
+ const { tool_calls, installs, uninstalls } = serverConfig.sessionResumeHistory;
107
+ await logger.info(`Seeding enforcement history from restored session - ` +
108
+ `${tool_calls.length} tool calls, ${installs.length} installs, ${uninstalls.length} uninstalls`);
109
+ const callToolObserver = policyEnforcer.getCallToolObserver();
110
+ callToolObserver.seedHistory(tool_calls);
111
+ const installObserver = policyEnforcer.getInstallObserver();
112
+ installObserver.seedHistory(installs, uninstalls);
113
+ await logger.info("Enforcement history seeded successfully");
114
+ }
115
+ else {
116
+ await logger.debug("No session resume history provided (new session)");
117
+ }
103
118
  const allSucceeded = serverManagerInitResults.succeeded;
104
119
  const allFailures = serverManagerInitResults.failures;
105
120
  // Initialize the serversCache with the succeeded servers
@@ -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
@@ -39,6 +39,8 @@ export async function serve(config) {
39
39
  Registry.setBundledDependencies(config.bundledDependencies);
40
40
  await logger.debug(`Bundled dependencies registered: ${JSON.stringify(config.bundledDependencies)}`);
41
41
  }
42
+ // Store server config in Registry (includes session resume history)
43
+ Registry.setServerConfig(config);
42
44
  await logger.info(`Starting Toolplex server in ${config.dev ? "development" : "production"} mode`);
43
45
  const server = new Server({
44
46
  name: "toolplex-server",
@@ -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;
@@ -21,6 +21,18 @@ export interface ToolplexServerConfig {
21
21
  clientName: string;
22
22
  logLevel: LogLevel;
23
23
  bundledDependencies?: BundledDependencies;
24
+ sessionResumeHistory?: {
25
+ tool_calls: Array<{
26
+ server_id: string;
27
+ tool_name: string;
28
+ }>;
29
+ installs: Array<{
30
+ server_id: string;
31
+ }>;
32
+ uninstalls: Array<{
33
+ server_id: string;
34
+ }>;
35
+ };
24
36
  }
25
37
  export declare const TransportTypeSchema: z.ZodEnum<["stdio", "sse"]>;
26
38
  export type TransportType = z.infer<typeof TransportTypeSchema>;
@@ -9,6 +9,12 @@ class CallToolObserver {
9
9
  }
10
10
  this.serverToolCalls.get(serverId).add(toolName);
11
11
  }
12
+ // Seed the observer with historical tool calls (for session resume)
13
+ seedHistory(history) {
14
+ history.forEach(({ server_id, tool_name }) => {
15
+ this.recordCall(server_id, tool_name);
16
+ });
17
+ }
12
18
  // Check if a server was called at all
13
19
  wasServerCalled(serverId) {
14
20
  return (this.serverToolCalls.has(serverId) &&
@@ -10,6 +10,15 @@ class InstallObserver {
10
10
  recordUninstall(serverId) {
11
11
  this.recordAction(serverId, "uninstall");
12
12
  }
13
+ // Seed the observer with historical install/uninstall actions (for session resume)
14
+ seedHistory(installs, uninstalls) {
15
+ installs.forEach(({ server_id }) => {
16
+ this.recordInstall(server_id);
17
+ });
18
+ uninstalls.forEach(({ server_id }) => {
19
+ this.recordUninstall(server_id);
20
+ });
21
+ }
13
22
  // Check if a server has been installed
14
23
  wasServerInstalled(serverId) {
15
24
  return (this.serverInstallActions.has(serverId) &&
@@ -101,6 +101,21 @@ class Registry {
101
101
  static getBundledDependencyPath(depName) {
102
102
  return this._bundledDependencies[depName];
103
103
  }
104
+ /**
105
+ * Set the server configuration (includes session resume history).
106
+ */
107
+ static setServerConfig(config) {
108
+ this._serverConfig = config;
109
+ }
110
+ /**
111
+ * Get the server configuration.
112
+ */
113
+ static getServerConfig() {
114
+ if (!this._serverConfig) {
115
+ throw new Error("ServerConfig not set in Registry");
116
+ }
117
+ return this._serverConfig;
118
+ }
104
119
  static reset() {
105
120
  this._clientContext = null;
106
121
  this._toolplexApiService = null;
@@ -120,6 +135,7 @@ class Registry {
120
135
  }
121
136
  this._policyEnforcer = null;
122
137
  this._bundledDependencies = {};
138
+ this._serverConfig = null;
123
139
  }
124
140
  }
125
141
  Registry._clientContext = null;
@@ -131,4 +147,5 @@ Registry._toolDefinitionsCache = null;
131
147
  Registry._serversCache = null;
132
148
  Registry._policyEnforcer = null;
133
149
  Registry._bundledDependencies = {};
150
+ Registry._serverConfig = null;
134
151
  export default Registry;
@@ -100,6 +100,21 @@ export async function handleInitialize(params) {
100
100
  promptsCache.init(processedPrompts);
101
101
  // Init PolicyEnforce after setting permissions and flags
102
102
  policyEnforcer.init(clientContext);
103
+ // Seed enforcement history if provided (for restored sessions)
104
+ const serverConfig = Registry.getServerConfig();
105
+ if (serverConfig.sessionResumeHistory) {
106
+ const { tool_calls, installs, uninstalls } = serverConfig.sessionResumeHistory;
107
+ await logger.info(`Seeding enforcement history from restored session - ` +
108
+ `${tool_calls.length} tool calls, ${installs.length} installs, ${uninstalls.length} uninstalls`);
109
+ const callToolObserver = policyEnforcer.getCallToolObserver();
110
+ callToolObserver.seedHistory(tool_calls);
111
+ const installObserver = policyEnforcer.getInstallObserver();
112
+ installObserver.seedHistory(installs, uninstalls);
113
+ await logger.info("Enforcement history seeded successfully");
114
+ }
115
+ else {
116
+ await logger.debug("No session resume history provided (new session)");
117
+ }
103
118
  const allSucceeded = serverManagerInitResults.succeeded;
104
119
  const allFailures = serverManagerInitResults.failures;
105
120
  // Initialize the serversCache with the succeeded servers
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "0.1.25";
1
+ export declare const version = "0.1.27";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '0.1.25';
1
+ export const version = '0.1.27';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolplex/client",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
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",