@letta-ai/letta-code 0.14.7 → 0.14.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.
@@ -0,0 +1,429 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * MCP HTTP Client - Connect to any MCP server over HTTP
4
+ *
5
+ * Usage:
6
+ * npx tsx mcp-http.ts <url> <command> [args]
7
+ *
8
+ * Commands:
9
+ * list-tools List available tools
10
+ * list-resources List available resources
11
+ * call <tool> '<json>' Call a tool with JSON arguments
12
+ *
13
+ * Options:
14
+ * --header "Key: Value" Add HTTP header (can be repeated)
15
+ *
16
+ * Examples:
17
+ * npx tsx mcp-http.ts http://localhost:3001/mcp list-tools
18
+ * npx tsx mcp-http.ts http://localhost:3001/mcp call vault '{"action":"list"}'
19
+ * npx tsx mcp-http.ts http://localhost:3001/mcp --header "Authorization: Bearer KEY" list-tools
20
+ */
21
+
22
+ interface JsonRpcRequest {
23
+ jsonrpc: "2.0";
24
+ method: string;
25
+ params?: object;
26
+ id: number;
27
+ }
28
+
29
+ interface JsonRpcResponse {
30
+ jsonrpc: "2.0";
31
+ result?: unknown;
32
+ error?: {
33
+ code: number;
34
+ message: string;
35
+ data?: unknown;
36
+ };
37
+ id: number;
38
+ }
39
+
40
+ interface ParsedArgs {
41
+ url: string;
42
+ command: string;
43
+ commandArgs: string[];
44
+ headers: Record<string, string>;
45
+ }
46
+
47
+ function parseArgs(): ParsedArgs {
48
+ const args = process.argv.slice(2);
49
+ const headers: Record<string, string> = {};
50
+ let url = "";
51
+ let command = "";
52
+ const commandArgs: string[] = [];
53
+
54
+ let i = 0;
55
+ while (i < args.length) {
56
+ const arg = args[i];
57
+ if (!arg) {
58
+ i++;
59
+ continue;
60
+ }
61
+
62
+ if (arg === "--header" || arg === "-H") {
63
+ const headerValue = args[++i];
64
+ if (headerValue) {
65
+ const colonIndex = headerValue.indexOf(":");
66
+ if (colonIndex > 0) {
67
+ const key = headerValue.slice(0, colonIndex).trim();
68
+ const value = headerValue.slice(colonIndex + 1).trim();
69
+ headers[key] = value;
70
+ }
71
+ }
72
+ } else if (arg === "--help" || arg === "-h") {
73
+ printUsage();
74
+ process.exit(0);
75
+ } else if (!url && arg.startsWith("http")) {
76
+ url = arg;
77
+ } else if (!command) {
78
+ command = arg;
79
+ } else {
80
+ commandArgs.push(arg);
81
+ }
82
+ i++;
83
+ }
84
+
85
+ return { url, command, commandArgs, headers };
86
+ }
87
+
88
+ // Session state
89
+ let sessionId: string | null = null;
90
+ let initialized = false;
91
+ let requestHeaders: Record<string, string> = {};
92
+ let serverUrl = "";
93
+
94
+ async function rawMcpRequest(
95
+ method: string,
96
+ params?: object,
97
+ ): Promise<{ response: JsonRpcResponse; newSessionId?: string }> {
98
+ const request: JsonRpcRequest = {
99
+ jsonrpc: "2.0",
100
+ method,
101
+ params,
102
+ id: Date.now(),
103
+ };
104
+
105
+ const headers: Record<string, string> = {
106
+ "Content-Type": "application/json",
107
+ Accept: "application/json, text/event-stream",
108
+ ...requestHeaders,
109
+ };
110
+
111
+ if (sessionId) {
112
+ headers["Mcp-Session-Id"] = sessionId;
113
+ }
114
+
115
+ try {
116
+ const fetchResponse = await fetch(serverUrl, {
117
+ method: "POST",
118
+ headers,
119
+ body: JSON.stringify(request),
120
+ });
121
+
122
+ // Capture session ID from response
123
+ const newSessionId =
124
+ fetchResponse.headers.get("Mcp-Session-Id") || undefined;
125
+
126
+ if (!fetchResponse.ok) {
127
+ const text = await fetchResponse.text();
128
+ if (fetchResponse.status === 401) {
129
+ throw new Error(
130
+ `Authentication required.\n` +
131
+ `Add --header "Authorization: Bearer YOUR_KEY" or similar.`,
132
+ );
133
+ }
134
+
135
+ // Try to parse as JSON-RPC error
136
+ try {
137
+ const errorResponse = JSON.parse(text) as JsonRpcResponse;
138
+ return { response: errorResponse, newSessionId };
139
+ } catch {
140
+ throw new Error(
141
+ `HTTP ${fetchResponse.status}: ${fetchResponse.statusText}\n${text}`,
142
+ );
143
+ }
144
+ }
145
+
146
+ const contentType = fetchResponse.headers.get("content-type") || "";
147
+
148
+ // Handle JSON response
149
+ if (contentType.includes("application/json")) {
150
+ const jsonResponse = (await fetchResponse.json()) as JsonRpcResponse;
151
+ return { response: jsonResponse, newSessionId };
152
+ }
153
+
154
+ // Handle SSE stream (simplified - just collect all events)
155
+ if (contentType.includes("text/event-stream")) {
156
+ const text = await fetchResponse.text();
157
+ const dataLines = text
158
+ .split("\n")
159
+ .filter((line) => line.startsWith("data: "))
160
+ .map((line) => line.slice(6));
161
+
162
+ for (let i = dataLines.length - 1; i >= 0; i--) {
163
+ const line = dataLines[i];
164
+ if (!line) continue;
165
+ try {
166
+ const parsed = JSON.parse(line);
167
+ if (parsed.jsonrpc === "2.0") {
168
+ return { response: parsed as JsonRpcResponse, newSessionId };
169
+ }
170
+ } catch {
171
+ // Continue to previous line
172
+ }
173
+ }
174
+ throw new Error("No valid JSON-RPC response found in SSE stream");
175
+ }
176
+
177
+ throw new Error(`Unexpected content type: ${contentType}`);
178
+ } catch (error) {
179
+ if (error instanceof TypeError && error.message.includes("fetch")) {
180
+ throw new Error(
181
+ `Cannot connect to ${serverUrl}\nIs the MCP server running?`,
182
+ );
183
+ }
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ async function ensureInitialized(): Promise<void> {
189
+ if (initialized) return;
190
+
191
+ const { response, newSessionId } = await rawMcpRequest("initialize", {
192
+ protocolVersion: "2024-11-05",
193
+ capabilities: {},
194
+ clientInfo: {
195
+ name: "mcp-http-cli",
196
+ version: "1.0.0",
197
+ },
198
+ });
199
+
200
+ if (newSessionId) {
201
+ sessionId = newSessionId;
202
+ }
203
+
204
+ if (response.error) {
205
+ throw new Error(`Initialization failed: ${response.error.message}`);
206
+ }
207
+
208
+ // Send initialized notification
209
+ await rawMcpRequest("notifications/initialized", {});
210
+
211
+ initialized = true;
212
+ }
213
+
214
+ async function mcpRequest(
215
+ method: string,
216
+ params?: object,
217
+ ): Promise<JsonRpcResponse> {
218
+ await ensureInitialized();
219
+
220
+ const { response, newSessionId } = await rawMcpRequest(method, params);
221
+
222
+ if (newSessionId) {
223
+ sessionId = newSessionId;
224
+ }
225
+
226
+ return response;
227
+ }
228
+
229
+ async function listTools(): Promise<void> {
230
+ const response = await mcpRequest("tools/list");
231
+
232
+ if (response.error) {
233
+ console.error("Error:", response.error.message);
234
+ process.exit(1);
235
+ }
236
+
237
+ const result = response.result as {
238
+ tools: Array<{ name: string; description: string; inputSchema: object }>;
239
+ };
240
+
241
+ console.log("Available tools:\n");
242
+ for (const tool of result.tools) {
243
+ console.log(` ${tool.name}`);
244
+ if (tool.description) {
245
+ console.log(` ${tool.description}\n`);
246
+ } else {
247
+ console.log();
248
+ }
249
+ }
250
+
251
+ console.log(`\nTotal: ${result.tools.length} tools`);
252
+ console.log("\nUse 'call <tool> <json-args>' to invoke a tool");
253
+ }
254
+
255
+ async function listResources(): Promise<void> {
256
+ const response = await mcpRequest("resources/list");
257
+
258
+ if (response.error) {
259
+ console.error("Error:", response.error.message);
260
+ process.exit(1);
261
+ }
262
+
263
+ const result = response.result as {
264
+ resources: Array<{ uri: string; name: string; description?: string }>;
265
+ };
266
+
267
+ if (!result.resources || result.resources.length === 0) {
268
+ console.log("No resources available.");
269
+ return;
270
+ }
271
+
272
+ console.log("Available resources:\n");
273
+ for (const resource of result.resources) {
274
+ console.log(` ${resource.uri}`);
275
+ console.log(` ${resource.name}`);
276
+ if (resource.description) {
277
+ console.log(` ${resource.description}`);
278
+ }
279
+ console.log();
280
+ }
281
+ }
282
+
283
+ async function callTool(toolName: string, argsJson: string): Promise<void> {
284
+ let args: object;
285
+ try {
286
+ args = JSON.parse(argsJson || "{}");
287
+ } catch {
288
+ console.error(`Invalid JSON: ${argsJson}`);
289
+ process.exit(1);
290
+ }
291
+
292
+ const response = await mcpRequest("tools/call", {
293
+ name: toolName,
294
+ arguments: args,
295
+ });
296
+
297
+ if (response.error) {
298
+ console.error("Error:", response.error.message);
299
+ if (response.error.data) {
300
+ console.error("Details:", JSON.stringify(response.error.data, null, 2));
301
+ }
302
+ process.exit(1);
303
+ }
304
+
305
+ console.log(JSON.stringify(response.result, null, 2));
306
+ }
307
+
308
+ async function getToolSchema(toolName: string): Promise<void> {
309
+ const response = await mcpRequest("tools/list");
310
+
311
+ if (response.error) {
312
+ console.error("Error:", response.error.message);
313
+ process.exit(1);
314
+ }
315
+
316
+ const result = response.result as {
317
+ tools: Array<{ name: string; description: string; inputSchema: object }>;
318
+ };
319
+
320
+ const tool = result.tools.find((t) => t.name === toolName);
321
+ if (!tool) {
322
+ console.error(`Tool not found: ${toolName}`);
323
+ console.error(
324
+ `Available tools: ${result.tools.map((t) => t.name).join(", ")}`,
325
+ );
326
+ process.exit(1);
327
+ }
328
+
329
+ console.log(`Tool: ${tool.name}\n`);
330
+ if (tool.description) {
331
+ console.log(`Description: ${tool.description}\n`);
332
+ }
333
+ console.log("Input Schema:");
334
+ console.log(JSON.stringify(tool.inputSchema, null, 2));
335
+ }
336
+
337
+ function printUsage(): void {
338
+ console.log(`MCP HTTP Client - Connect to any MCP server over HTTP
339
+
340
+ Usage: npx tsx mcp-http.ts <url> [options] <command> [args]
341
+
342
+ Commands:
343
+ list-tools List available tools with descriptions
344
+ list-resources List available resources
345
+ info <tool> Show tool schema/parameters
346
+ call <tool> '<json>' Call a tool with JSON arguments
347
+
348
+ Options:
349
+ --header, -H "K: V" Add HTTP header (repeatable)
350
+ --help, -h Show this help
351
+
352
+ Examples:
353
+ # List tools from a server
354
+ npx tsx mcp-http.ts http://localhost:3001/mcp list-tools
355
+
356
+ # With authentication
357
+ npx tsx mcp-http.ts http://localhost:3001/mcp --header "Authorization: Bearer KEY" list-tools
358
+
359
+ # Get tool schema
360
+ npx tsx mcp-http.ts http://localhost:3001/mcp info vault
361
+
362
+ # Call a tool
363
+ npx tsx mcp-http.ts http://localhost:3001/mcp call vault '{"action":"list"}'
364
+ `);
365
+ }
366
+
367
+ async function main(): Promise<void> {
368
+ const { url, command, commandArgs, headers } = parseArgs();
369
+
370
+ if (!url) {
371
+ console.error("Error: URL is required\n");
372
+ printUsage();
373
+ process.exit(1);
374
+ }
375
+
376
+ if (!command) {
377
+ console.error("Error: Command is required\n");
378
+ printUsage();
379
+ process.exit(1);
380
+ }
381
+
382
+ // Set globals
383
+ serverUrl = url;
384
+ requestHeaders = headers;
385
+
386
+ try {
387
+ switch (command) {
388
+ case "list-tools":
389
+ await listTools();
390
+ break;
391
+
392
+ case "list-resources":
393
+ await listResources();
394
+ break;
395
+
396
+ case "info": {
397
+ const [toolName] = commandArgs;
398
+ if (!toolName) {
399
+ console.error("Error: Tool name required");
400
+ console.error("Usage: info <tool>");
401
+ process.exit(1);
402
+ }
403
+ await getToolSchema(toolName);
404
+ break;
405
+ }
406
+
407
+ case "call": {
408
+ const [toolName, argsJson] = commandArgs;
409
+ if (!toolName) {
410
+ console.error("Error: Tool name required");
411
+ console.error("Usage: call <tool> '<json-args>'");
412
+ process.exit(1);
413
+ }
414
+ await callTool(toolName, argsJson || "{}");
415
+ break;
416
+ }
417
+
418
+ default:
419
+ console.error(`Unknown command: ${command}\n`);
420
+ printUsage();
421
+ process.exit(1);
422
+ }
423
+ } catch (error) {
424
+ console.error("Error:", error instanceof Error ? error.message : error);
425
+ process.exit(1);
426
+ }
427
+ }
428
+
429
+ main();