@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.
- package/README.md +2 -2
- package/dist/app-server-client.js +387 -0
- package/dist/app-server-client.js.map +10 -0
- package/dist/types/app-server-client.d.ts +99 -0
- package/dist/types/app-server-client.d.ts.map +1 -0
- package/dist/types/types/app-server-protocol.d.ts +3 -0
- package/dist/types/types/app-server-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +2277 -0
- package/dist/types/types/protocol_v2.d.ts.map +1 -0
- package/letta.js +22835 -19810
- package/package.json +12 -2
- package/scripts/check-bundled-skill-scripts.js +169 -0
- package/scripts/check-test-coverage.cjs +1 -1
- package/scripts/check.js +1 -0
- package/scripts/run-unit-tests.cjs +1 -1
- package/skills/converting-mcps-to-skills/SKILL.md +1 -12
- package/skills/converting-mcps-to-skills/scripts/mcp-stdio.ts +192 -57
- package/skills/{creating-extensions → creating-mods}/SKILL.md +29 -29
- package/skills/{creating-extensions → creating-mods}/references/architecture.md +9 -9
- package/skills/{creating-extensions → creating-mods}/references/commands.md +10 -10
- package/skills/{creating-extensions → creating-mods}/references/events.md +10 -10
- package/skills/{creating-extensions → creating-mods}/references/permissions.md +3 -3
- package/skills/{creating-extensions → creating-mods}/references/plan-mode.md +72 -31
- package/skills/{creating-extensions → creating-mods}/references/providers.md +7 -7
- package/skills/{creating-extensions → creating-mods}/references/tools.md +20 -2
- package/skills/{creating-extensions → creating-mods}/references/ui.md +4 -4
- package/skills/creating-skills/scripts/validate-skill.ts +129 -5
- package/skills/customizing-commands/SKILL.md +18 -18
- package/skills/customizing-statusline/SKILL.md +11 -11
- package/skills/customizing-statusline/references/api.md +8 -8
- package/skills/customizing-statusline/references/examples.md +1 -1
- package/skills/customizing-statusline/references/migration.md +1 -1
- package/skills/editing-letta-code-desktop-preferences/SKILL.md +67 -0
- package/skills/image-generation/SKILL.md +120 -0
- package/skills/modifying-the-harness/SKILL.md +21 -2
- package/skills/modifying-the-harness/scripts/add_permission.py +2 -1
- package/skills/modifying-the-harness/scripts/show_config.py +4 -3
- package/dist/types/protocol.d.ts.map +0 -1
- package/skills/converting-mcps-to-skills/scripts/package.json +0 -13
- /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.
|
|
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.");
|
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
|
];
|
|
@@ -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.
|
|
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 {
|
|
29
|
-
|
|
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
|
|
117
|
-
let
|
|
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<
|
|
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
|
-
|
|
140
|
-
command,
|
|
141
|
-
args,
|
|
142
|
-
env: mergedEnv,
|
|
217
|
+
serverProcess = spawn(command, args, {
|
|
143
218
|
cwd,
|
|
144
|
-
|
|
219
|
+
env: mergedEnv,
|
|
220
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
145
221
|
});
|
|
146
222
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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 (
|
|
170
|
-
|
|
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(
|
|
179
|
-
const
|
|
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(
|
|
196
|
-
const
|
|
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(
|
|
215
|
-
const
|
|
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
|
|
378
|
+
const response = await sendRequest("tools/call", {
|
|
248
379
|
name: toolName,
|
|
249
380
|
arguments: args,
|
|
250
381
|
});
|
|
251
382
|
|
|
252
|
-
|
|
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
|
-
|
|
448
|
+
await connect(serverCommand, env, cwd);
|
|
314
449
|
|
|
315
450
|
switch (action) {
|
|
316
451
|
case "list-tools":
|
|
317
|
-
await listTools(
|
|
452
|
+
await listTools();
|
|
318
453
|
break;
|
|
319
454
|
|
|
320
455
|
case "list-resources":
|
|
321
|
-
await listResources(
|
|
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(
|
|
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(
|
|
477
|
+
await callTool(toolName, argsJson || "{}");
|
|
343
478
|
break;
|
|
344
479
|
}
|
|
345
480
|
|