@letta-ai/letta-code 0.27.7 → 0.27.9

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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/app-server-client.js +387 -0
  3. package/dist/app-server-client.js.map +10 -0
  4. package/dist/types/app-server-client.d.ts +99 -0
  5. package/dist/types/app-server-client.d.ts.map +1 -0
  6. package/dist/types/types/app-server-protocol.d.ts +3 -0
  7. package/dist/types/types/app-server-protocol.d.ts.map +1 -0
  8. package/dist/types/types/protocol.d.ts.map +1 -0
  9. package/dist/types/types/protocol_v2.d.ts +2277 -0
  10. package/dist/types/types/protocol_v2.d.ts.map +1 -0
  11. package/letta.js +22835 -19810
  12. package/package.json +12 -2
  13. package/scripts/check-bundled-skill-scripts.js +169 -0
  14. package/scripts/check-test-coverage.cjs +1 -1
  15. package/scripts/check.js +1 -0
  16. package/scripts/run-unit-tests.cjs +1 -1
  17. package/skills/converting-mcps-to-skills/SKILL.md +1 -12
  18. package/skills/converting-mcps-to-skills/scripts/mcp-stdio.ts +192 -57
  19. package/skills/{creating-extensions → creating-mods}/SKILL.md +29 -29
  20. package/skills/{creating-extensions → creating-mods}/references/architecture.md +9 -9
  21. package/skills/{creating-extensions → creating-mods}/references/commands.md +10 -10
  22. package/skills/{creating-extensions → creating-mods}/references/events.md +10 -10
  23. package/skills/{creating-extensions → creating-mods}/references/permissions.md +3 -3
  24. package/skills/{creating-extensions → creating-mods}/references/plan-mode.md +72 -31
  25. package/skills/{creating-extensions → creating-mods}/references/providers.md +7 -7
  26. package/skills/{creating-extensions → creating-mods}/references/tools.md +20 -2
  27. package/skills/{creating-extensions → creating-mods}/references/ui.md +4 -4
  28. package/skills/creating-skills/scripts/validate-skill.ts +129 -5
  29. package/skills/customizing-commands/SKILL.md +18 -18
  30. package/skills/customizing-statusline/SKILL.md +11 -11
  31. package/skills/customizing-statusline/references/api.md +8 -8
  32. package/skills/customizing-statusline/references/examples.md +1 -1
  33. package/skills/customizing-statusline/references/migration.md +1 -1
  34. package/skills/editing-letta-code-desktop-preferences/SKILL.md +67 -0
  35. package/skills/image-generation/SKILL.md +120 -0
  36. package/skills/modifying-the-harness/SKILL.md +21 -2
  37. package/skills/modifying-the-harness/scripts/add_permission.py +2 -1
  38. package/skills/modifying-the-harness/scripts/show_config.py +4 -3
  39. package/dist/types/protocol.d.ts.map +0 -1
  40. package/skills/converting-mcps-to-skills/scripts/package.json +0 -13
  41. /package/dist/types/{protocol.d.ts → types/protocol.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.27.7",
3
+ "version": "0.27.9",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.0",
@@ -14,11 +14,20 @@
14
14
  "scripts",
15
15
  "skills",
16
16
  "vendor",
17
+ "dist/app-server-client.js",
18
+ "dist/app-server-client.js.map",
17
19
  "dist/types",
18
20
  "docs"
19
21
  ],
20
22
  "exports": {
21
23
  ".": "./letta.js",
24
+ "./app-server-protocol": {
25
+ "types": "./dist/types/app-server-protocol.d.ts"
26
+ },
27
+ "./app-server-client": {
28
+ "types": "./dist/types/app-server-client.d.ts",
29
+ "import": "./dist/app-server-client.js"
30
+ },
22
31
  "./protocol": {
23
32
  "types": "./dist/types/protocol.d.ts"
24
33
  }
@@ -35,8 +44,8 @@
35
44
  "access": "public"
36
45
  },
37
46
  "dependencies": {
47
+ "@earendil-works/pi-ai": "^0.79.1",
38
48
  "@letta-ai/letta-client": "^1.10.2",
39
- "@earendil-works/pi-ai": "^0.78.1",
40
49
  "@pierre/diffs": "1.2.2",
41
50
  "glob": "^13.0.0",
42
51
  "ink-link": "^5.0.0",
@@ -81,6 +90,7 @@
81
90
  "check:filename-casing": "node scripts/check-filename-casing.js",
82
91
  "check:test-mock-isolation": "bun run scripts/check-test-mock-isolation.js",
83
92
  "check:test-coverage": "node scripts/check-test-coverage.cjs",
93
+ "check:bundled-skill-scripts": "node scripts/check-bundled-skill-scripts.js",
84
94
  "check": "bun run scripts/check.js",
85
95
  "dev": "LETTA_DEBUG=${LETTA_DEBUG:-1} LETTA_RESPONSES_WS=${LETTA_RESPONSES_WS:-1} bun --loader=.md:text --loader=.mdx:text --loader=.txt:text run src/index.ts",
86
96
  "build": "node scripts/postinstall-patches.js && bun run build.js",
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Enforces that bundled skill scripts are self-contained.
5
+ *
6
+ * Built-in skills are shipped as standalone resources, including inside app
7
+ * bundles where they can live below node_modules/. A script that imports a bare
8
+ * package specifier can fail there because Bun disables auto-install when any
9
+ * node_modules directory exists up the tree. Keep bundled scripts limited to
10
+ * relative imports and runtime built-ins, or invoke lazy resolvers explicitly
11
+ * from SKILL.md (npx/uvx/uv run/etc.) instead of relying on a manifest install.
12
+ */
13
+
14
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
15
+ import { builtinModules } from "node:module";
16
+ import { join } from "node:path";
17
+
18
+ const rootDir = process.cwd();
19
+ const builtinSkillDir = join(rootDir, "src", "skills", "builtin");
20
+
21
+ const scriptExtensions = new Set([
22
+ ".cjs",
23
+ ".cts",
24
+ ".js",
25
+ ".mjs",
26
+ ".mts",
27
+ ".ts",
28
+ ]);
29
+ const forbiddenManifestNames = new Set([
30
+ "bun.lock",
31
+ "bun.lockb",
32
+ "package-lock.json",
33
+ "package.json",
34
+ "pnpm-lock.yaml",
35
+ "yarn.lock",
36
+ ]);
37
+
38
+ const builtinNames = new Set(
39
+ builtinModules.flatMap((name) => {
40
+ const bare = name.startsWith("node:") ? name.slice("node:".length) : name;
41
+ return [bare, `node:${bare}`];
42
+ }),
43
+ );
44
+
45
+ function walk(dir) {
46
+ if (!existsSync(dir)) return [];
47
+
48
+ const files = [];
49
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
50
+ const fullPath = join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ files.push(...walk(fullPath));
53
+ } else {
54
+ files.push(fullPath);
55
+ }
56
+ }
57
+ return files;
58
+ }
59
+
60
+ function toRepoPath(file) {
61
+ return file.slice(rootDir.length + 1).replace(/\\/g, "/");
62
+ }
63
+
64
+ function extensionOf(file) {
65
+ const basename = file.split(/[\\/]/).pop() ?? file;
66
+ if (basename.endsWith(".d.ts")) return ".d.ts";
67
+ const dotIndex = basename.lastIndexOf(".");
68
+ return dotIndex === -1 ? "" : basename.slice(dotIndex);
69
+ }
70
+
71
+ function isScriptFile(file) {
72
+ return scriptExtensions.has(extensionOf(file));
73
+ }
74
+
75
+ function isRuntimeBuiltin(specifier) {
76
+ if (specifier.startsWith("node:")) return true;
77
+ return (
78
+ builtinNames.has(specifier) || builtinNames.has(specifier.split("/")[0])
79
+ );
80
+ }
81
+
82
+ function isAllowedScriptImport(specifier) {
83
+ return (
84
+ specifier.startsWith(".") ||
85
+ specifier.startsWith("/") ||
86
+ isRuntimeBuiltin(specifier)
87
+ );
88
+ }
89
+
90
+ function lineNumberForIndex(content, index) {
91
+ let line = 1;
92
+ for (let i = 0; i < index; i++) {
93
+ if (content.charCodeAt(i) === 10) line++;
94
+ }
95
+ return line;
96
+ }
97
+
98
+ function collectImportViolations(file) {
99
+ const content = readFileSync(file, "utf8");
100
+ const patterns = [
101
+ /\bimport\s+(?:type\s+)?[\s\S]*?\s+from\s+["']([^"']+)["']/g,
102
+ /\bimport\s+["']([^"']+)["']/g,
103
+ /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
104
+ /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
105
+ /\bexport\s+(?:type\s+)?[\s\S]*?\s+from\s+["']([^"']+)["']/g,
106
+ ];
107
+
108
+ const violations = [];
109
+ for (const pattern of patterns) {
110
+ for (const match of content.matchAll(pattern)) {
111
+ const specifier = match[1];
112
+ if (!specifier || isAllowedScriptImport(specifier)) {
113
+ continue;
114
+ }
115
+ violations.push({
116
+ line: lineNumberForIndex(content, match.index ?? 0),
117
+ specifier,
118
+ });
119
+ }
120
+ }
121
+
122
+ return violations;
123
+ }
124
+
125
+ const allFiles = walk(builtinSkillDir);
126
+ const scriptFiles = allFiles.filter(
127
+ (file) => toRepoPath(file).includes("/scripts/") && isScriptFile(file),
128
+ );
129
+ const forbiddenManifests = allFiles.filter((file) => {
130
+ const repoPath = toRepoPath(file);
131
+ const filename = repoPath.split("/").pop();
132
+ return repoPath.includes("/scripts/") && forbiddenManifestNames.has(filename);
133
+ });
134
+
135
+ let violations = 0;
136
+
137
+ for (const file of scriptFiles.sort()) {
138
+ for (const violation of collectImportViolations(file)) {
139
+ if (violations === 0) {
140
+ console.error("\n❌ Bundled skill script dependency violations found:\n");
141
+ }
142
+ console.error(`${toRepoPath(file)}:${violation.line}`);
143
+ console.error(` imports '${violation.specifier}'`);
144
+ console.error(
145
+ " ↳ Bundled skill scripts must be self-contained: use relative files, Node/Bun built-ins, or document an explicit lazy resolver command.\n",
146
+ );
147
+ violations++;
148
+ }
149
+ }
150
+
151
+ for (const file of forbiddenManifests.sort()) {
152
+ if (violations === 0) {
153
+ console.error("\n❌ Bundled skill script dependency violations found:\n");
154
+ }
155
+ console.error(toRepoPath(file));
156
+ console.error(
157
+ " ↳ Do not rely on package manager manifests inside bundled skill scripts; scripts should run without a separate install step.\n",
158
+ );
159
+ violations++;
160
+ }
161
+
162
+ if (violations > 0) {
163
+ console.error(
164
+ `Found ${violations} bundled skill script dependency violation${violations === 1 ? "" : "s"}.`,
165
+ );
166
+ process.exit(1);
167
+ }
168
+
169
+ console.log("✅ Bundled skill scripts are self-contained.");
@@ -73,9 +73,9 @@ const ciDirs = [
73
73
  "src/cli",
74
74
  "src/cron",
75
75
  "src/experiments",
76
- "src/extensions",
77
76
  "src/hooks",
78
77
  "src/lsp",
78
+ "src/mods",
79
79
  "src/permissions",
80
80
  "src/providers",
81
81
  "src/queue",
package/scripts/check.js CHANGED
@@ -19,6 +19,7 @@ const checks = [
19
19
  { name: "filename casing", script: ["check:filename-casing"] },
20
20
  { name: "test mock isolation", script: ["check:test-mock-isolation"] },
21
21
  { name: "test coverage", script: ["check:test-coverage"] },
22
+ { name: "bundled skill scripts", script: ["check:bundled-skill-scripts"] },
22
23
  { name: "biome", script: ["lint"] },
23
24
  { name: "typescript", script: ["typecheck"] },
24
25
  ];
@@ -13,9 +13,9 @@ const dirs = [
13
13
  "src/cli",
14
14
  "src/cron",
15
15
  "src/experiments",
16
- "src/extensions",
17
16
  "src/hooks",
18
17
  "src/lsp",
18
+ "src/mods",
19
19
  "src/permissions",
20
20
  "src/providers",
21
21
  "src/queue",
@@ -34,10 +34,6 @@ Where `<SKILL_DIR>` is the Skill Directory shown when the skill was loaded (visi
34
34
 
35
35
  **For stdio servers:**
36
36
  ```bash
37
- # First, install dependencies (one time)
38
- cd <SKILL_DIR>/scripts && npm install
39
-
40
- # Then connect
41
37
  npx tsx <SKILL_DIR>/scripts/mcp-stdio.ts "<command>" list-tools
42
38
 
43
39
  # Examples
@@ -110,12 +106,9 @@ npx tsx mcp-http.ts http://localhost:3001/mcp call vault '{"action":"search","qu
110
106
 
111
107
  ### mcp-stdio.ts - stdio Transport
112
108
 
113
- Connects to MCP servers that run as subprocesses. Requires npm install first.
109
+ Connects to MCP servers that run as subprocesses. No dependencies required.
114
110
 
115
111
  ```bash
116
- # One-time setup
117
- cd <SKILL_DIR>/scripts && npm install
118
-
119
112
  npx tsx mcp-stdio.ts "<command>" [options] <action> [args]
120
113
 
121
114
  Actions:
@@ -163,10 +156,6 @@ Here are some well-known MCP servers:
163
156
  - Add `--header "Authorization: Bearer YOUR_KEY"` for HTTP
164
157
  - Or `--env "API_KEY=xxx"` for stdio servers that need env vars
165
158
 
166
- **stdio "npm install" error:**
167
- - Run `cd <SKILL_DIR>/scripts && npm install` first
168
- - The stdio client requires the MCP SDK
169
-
170
159
  **Tool call fails:**
171
160
  - Use `info <tool>` to see the expected input schema
172
161
  - Ensure JSON arguments match the schema
@@ -2,9 +2,6 @@
2
2
  /**
3
3
  * MCP stdio Client - Connect to any MCP server over stdio
4
4
  *
5
- * NOTE: Requires npm install in this directory first:
6
- * cd <this-directory> && npm install
7
- *
8
5
  * Usage:
9
6
  * npx tsx mcp-stdio.ts "<command>" <action> [args]
10
7
  *
@@ -25,8 +22,31 @@
25
22
  * npx tsx mcp-stdio.ts "node server.js" --env "API_KEY=xxx" list-tools
26
23
  */
27
24
 
28
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
29
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
25
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
26
+
27
+ interface JsonRpcRequest {
28
+ jsonrpc: "2.0";
29
+ method: string;
30
+ params?: object;
31
+ id: number;
32
+ }
33
+
34
+ interface JsonRpcNotification {
35
+ jsonrpc: "2.0";
36
+ method: string;
37
+ params?: object;
38
+ }
39
+
40
+ interface JsonRpcResponse {
41
+ jsonrpc: "2.0";
42
+ result?: unknown;
43
+ error?: {
44
+ code: number;
45
+ message: string;
46
+ data?: unknown;
47
+ };
48
+ id: number;
49
+ }
30
50
 
31
51
  interface ParsedArgs {
32
52
  serverCommand: string;
@@ -81,7 +101,7 @@ function parseArgs(): ParsedArgs {
81
101
  }
82
102
 
83
103
  function parseCommand(commandStr: string): { command: string; args: string[] } {
84
- // Simple parsing - split on spaces, respecting quotes
104
+ // Simple parsing - split on spaces, respecting quotes.
85
105
  const parts: string[] = [];
86
106
  let current = "";
87
107
  let inQuote = false;
@@ -113,21 +133,79 @@ function parseCommand(commandStr: string): { command: string; args: string[] } {
113
133
  };
114
134
  }
115
135
 
116
- let client: Client | null = null;
117
- let transport: StdioClientTransport | null = null;
136
+ let serverProcess: ChildProcessWithoutNullStreams | null = null;
137
+ let stdoutBuffer = "";
138
+ let nextRequestId = 1;
139
+ const pendingRequests = new Map<
140
+ number,
141
+ {
142
+ resolve: (response: JsonRpcResponse) => void;
143
+ reject: (error: Error) => void;
144
+ }
145
+ >();
146
+
147
+ function handleStdout(chunk: Buffer): void {
148
+ stdoutBuffer += chunk.toString("utf8");
149
+
150
+ while (true) {
151
+ const newlineIndex = stdoutBuffer.indexOf("\n");
152
+ if (newlineIndex === -1) {
153
+ return;
154
+ }
155
+
156
+ const line = stdoutBuffer.slice(0, newlineIndex).trim();
157
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
158
+
159
+ if (!line) {
160
+ continue;
161
+ }
162
+
163
+ let message: unknown;
164
+ try {
165
+ message = JSON.parse(line);
166
+ } catch {
167
+ process.stderr.write(`[server stdout] ${line}\n`);
168
+ continue;
169
+ }
170
+
171
+ if (
172
+ typeof message !== "object" ||
173
+ message === null ||
174
+ !("id" in message) ||
175
+ typeof message.id !== "number"
176
+ ) {
177
+ continue;
178
+ }
179
+
180
+ const pending = pendingRequests.get(message.id);
181
+ if (!pending) {
182
+ continue;
183
+ }
184
+
185
+ pendingRequests.delete(message.id);
186
+ pending.resolve(message as JsonRpcResponse);
187
+ }
188
+ }
189
+
190
+ function rejectPendingRequests(error: Error): void {
191
+ for (const pending of pendingRequests.values()) {
192
+ pending.reject(error);
193
+ }
194
+ pendingRequests.clear();
195
+ }
118
196
 
119
197
  async function connect(
120
198
  serverCommand: string,
121
199
  env: Record<string, string>,
122
200
  cwd?: string,
123
- ): Promise<Client> {
201
+ ): Promise<void> {
124
202
  const { command, args } = parseCommand(serverCommand);
125
203
 
126
204
  if (!command) {
127
205
  throw new Error("No command specified");
128
206
  }
129
207
 
130
- // Merge with process.env
208
+ // Merge with process.env.
131
209
  const mergedEnv: Record<string, string> = {};
132
210
  for (const [key, value] of Object.entries(process.env)) {
133
211
  if (value !== undefined) {
@@ -136,47 +214,86 @@ async function connect(
136
214
  }
137
215
  Object.assign(mergedEnv, env);
138
216
 
139
- transport = new StdioClientTransport({
140
- command,
141
- args,
142
- env: mergedEnv,
217
+ serverProcess = spawn(command, args, {
143
218
  cwd,
144
- stderr: "pipe",
219
+ env: mergedEnv,
220
+ stdio: ["pipe", "pipe", "pipe"],
145
221
  });
146
222
 
147
- // Forward stderr for debugging
148
- if (transport.stderr) {
149
- transport.stderr.on("data", (chunk: Buffer) => {
150
- process.stderr.write(`[server] ${chunk.toString()}`);
151
- });
152
- }
223
+ serverProcess.stdout.on("data", handleStdout);
224
+ serverProcess.stderr.on("data", (chunk: Buffer) => {
225
+ process.stderr.write(`[server] ${chunk.toString()}`);
226
+ });
227
+ serverProcess.on("error", (error) => {
228
+ rejectPendingRequests(error);
229
+ });
230
+ serverProcess.on("exit", (code, signal) => {
231
+ rejectPendingRequests(
232
+ new Error(`MCP server exited with code ${code} signal ${signal}`),
233
+ );
234
+ });
153
235
 
154
- client = new Client(
155
- {
236
+ const initializeResponse = await sendRequest("initialize", {
237
+ protocolVersion: "2024-11-05",
238
+ capabilities: {},
239
+ clientInfo: {
156
240
  name: "mcp-stdio-cli",
157
241
  version: "1.0.0",
158
242
  },
159
- {
160
- capabilities: {},
161
- },
162
- );
243
+ });
244
+
245
+ if (initializeResponse.error) {
246
+ throw new Error(
247
+ `Initialization failed: ${initializeResponse.error.message}`,
248
+ );
249
+ }
250
+
251
+ sendNotification("notifications/initialized", {});
252
+ }
253
+
254
+ function sendMessage(message: JsonRpcRequest | JsonRpcNotification): void {
255
+ if (!serverProcess) {
256
+ throw new Error("MCP server is not connected");
257
+ }
163
258
 
164
- await client.connect(transport);
165
- return client;
259
+ serverProcess.stdin.write(`${JSON.stringify(message)}\n`);
260
+ }
261
+
262
+ function sendNotification(method: string, params?: object): void {
263
+ sendMessage({ jsonrpc: "2.0", method, params });
264
+ }
265
+
266
+ function sendRequest(
267
+ method: string,
268
+ params?: object,
269
+ ): Promise<JsonRpcResponse> {
270
+ const id = nextRequestId++;
271
+ const request: JsonRpcRequest = { jsonrpc: "2.0", method, params, id };
272
+
273
+ return new Promise((resolve, reject) => {
274
+ pendingRequests.set(id, { resolve, reject });
275
+ sendMessage(request);
276
+ });
166
277
  }
167
278
 
168
279
  async function cleanup(): Promise<void> {
169
- if (client) {
170
- try {
171
- await client.close();
172
- } catch {
173
- // Ignore cleanup errors
174
- }
280
+ if (serverProcess && !serverProcess.killed) {
281
+ serverProcess.kill();
175
282
  }
283
+ serverProcess = null;
176
284
  }
177
285
 
178
- async function listTools(client: Client): Promise<void> {
179
- const result = await client.listTools();
286
+ async function listTools(): Promise<void> {
287
+ const response = await sendRequest("tools/list");
288
+
289
+ if (response.error) {
290
+ console.error("Error:", response.error.message);
291
+ process.exit(1);
292
+ }
293
+
294
+ const result = response.result as {
295
+ tools: Array<{ name: string; description?: string; inputSchema: object }>;
296
+ };
180
297
 
181
298
  console.log("Available tools:\n");
182
299
  for (const tool of result.tools) {
@@ -192,8 +309,17 @@ async function listTools(client: Client): Promise<void> {
192
309
  console.log("\nUse 'call <tool> <json-args>' to invoke a tool");
193
310
  }
194
311
 
195
- async function listResources(client: Client): Promise<void> {
196
- const result = await client.listResources();
312
+ async function listResources(): Promise<void> {
313
+ const response = await sendRequest("resources/list");
314
+
315
+ if (response.error) {
316
+ console.error("Error:", response.error.message);
317
+ process.exit(1);
318
+ }
319
+
320
+ const result = response.result as {
321
+ resources: Array<{ uri: string; name: string; description?: string }>;
322
+ };
197
323
 
198
324
  if (!result.resources || result.resources.length === 0) {
199
325
  console.log("No resources available.");
@@ -211,8 +337,17 @@ async function listResources(client: Client): Promise<void> {
211
337
  }
212
338
  }
213
339
 
214
- async function getToolSchema(client: Client, toolName: string): Promise<void> {
215
- const result = await client.listTools();
340
+ async function getToolSchema(toolName: string): Promise<void> {
341
+ const response = await sendRequest("tools/list");
342
+
343
+ if (response.error) {
344
+ console.error("Error:", response.error.message);
345
+ process.exit(1);
346
+ }
347
+
348
+ const result = response.result as {
349
+ tools: Array<{ name: string; description?: string; inputSchema: object }>;
350
+ };
216
351
 
217
352
  const tool = result.tools.find((t) => t.name === toolName);
218
353
  if (!tool) {
@@ -231,11 +366,7 @@ async function getToolSchema(client: Client, toolName: string): Promise<void> {
231
366
  console.log(JSON.stringify(tool.inputSchema, null, 2));
232
367
  }
233
368
 
234
- async function callTool(
235
- client: Client,
236
- toolName: string,
237
- argsJson: string,
238
- ): Promise<void> {
369
+ async function callTool(toolName: string, argsJson: string): Promise<void> {
239
370
  let args: Record<string, unknown>;
240
371
  try {
241
372
  args = JSON.parse(argsJson || "{}");
@@ -244,20 +375,25 @@ async function callTool(
244
375
  process.exit(1);
245
376
  }
246
377
 
247
- const result = await client.callTool({
378
+ const response = await sendRequest("tools/call", {
248
379
  name: toolName,
249
380
  arguments: args,
250
381
  });
251
382
 
252
- console.log(JSON.stringify(result, null, 2));
383
+ if (response.error) {
384
+ console.error("Error:", response.error.message);
385
+ if (response.error.data) {
386
+ console.error("Details:", JSON.stringify(response.error.data, null, 2));
387
+ }
388
+ process.exit(1);
389
+ }
390
+
391
+ console.log(JSON.stringify(response.result, null, 2));
253
392
  }
254
393
 
255
394
  function printUsage(): void {
256
395
  console.log(`MCP stdio Client - Connect to any MCP server over stdio
257
396
 
258
- NOTE: Requires npm install in this directory first:
259
- cd <this-directory> && npm install
260
-
261
397
  Usage: npx tsx mcp-stdio.ts "<command>" [options] <action> [args]
262
398
 
263
399
  Actions:
@@ -298,7 +434,6 @@ async function main(): Promise<void> {
298
434
  process.exit(1);
299
435
  }
300
436
 
301
- // Handle process exit
302
437
  process.on("SIGINT", async () => {
303
438
  await cleanup();
304
439
  process.exit(0);
@@ -310,15 +445,15 @@ async function main(): Promise<void> {
310
445
  });
311
446
 
312
447
  try {
313
- const connectedClient = await connect(serverCommand, env, cwd);
448
+ await connect(serverCommand, env, cwd);
314
449
 
315
450
  switch (action) {
316
451
  case "list-tools":
317
- await listTools(connectedClient);
452
+ await listTools();
318
453
  break;
319
454
 
320
455
  case "list-resources":
321
- await listResources(connectedClient);
456
+ await listResources();
322
457
  break;
323
458
 
324
459
  case "info": {
@@ -328,7 +463,7 @@ async function main(): Promise<void> {
328
463
  console.error("Usage: info <tool>");
329
464
  process.exit(1);
330
465
  }
331
- await getToolSchema(connectedClient, toolName);
466
+ await getToolSchema(toolName);
332
467
  break;
333
468
  }
334
469
 
@@ -339,7 +474,7 @@ async function main(): Promise<void> {
339
474
  console.error("Usage: call <tool> '<json-args>'");
340
475
  process.exit(1);
341
476
  }
342
- await callTool(connectedClient, toolName, argsJson || "{}");
477
+ await callTool(toolName, argsJson || "{}");
343
478
  break;
344
479
  }
345
480