@letta-ai/letta-code 0.32.11 → 0.32.13

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 (39) hide show
  1. package/dist/channels-slack.js +135 -54
  2. package/dist/channels-slack.js.map +3 -3
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/message.d.ts.map +1 -1
  6. package/dist/types/agent/subagents/manager.d.ts.map +1 -1
  7. package/dist/types/agent/subagents/subagent-process.d.ts +23 -0
  8. package/dist/types/agent/subagents/subagent-process.d.ts.map +1 -0
  9. package/dist/types/backend/api/metadata.d.ts +12 -0
  10. package/dist/types/backend/api/metadata.d.ts.map +1 -1
  11. package/dist/types/channels/slack/status-controller.d.ts +2 -0
  12. package/dist/types/channels/slack/status-controller.d.ts.map +1 -1
  13. package/dist/types/cli/helpers/error-formatter.d.ts.map +1 -1
  14. package/dist/types/cli/helpers/stream-processor.d.ts +2 -0
  15. package/dist/types/cli/helpers/stream-processor.d.ts.map +1 -1
  16. package/dist/types/telemetry/index.d.ts +16 -1
  17. package/dist/types/telemetry/index.d.ts.map +1 -1
  18. package/dist/types/tools/impl/process_manager.d.ts +2 -0
  19. package/dist/types/tools/impl/process_manager.d.ts.map +1 -1
  20. package/dist/types/tools/impl/task-stop.d.ts.map +1 -1
  21. package/dist/types/tools/impl/task.d.ts.map +1 -1
  22. package/dist/types/utils/cloud-api-shutdown.d.ts +17 -0
  23. package/dist/types/utils/cloud-api-shutdown.d.ts.map +1 -0
  24. package/dist/types/websocket/listener/constants.d.ts +1 -0
  25. package/dist/types/websocket/listener/constants.d.ts.map +1 -1
  26. package/letta.js +2 -2
  27. package/package.json +1 -1
  28. package/scripts/build-pypi.py +172 -0
  29. package/scripts/isolated-unit-tests.json +5 -0
  30. package/scripts/smoke-pypi.py +236 -0
  31. package/scripts/source-file-size-baseline.json +1 -1
  32. package/scripts/stage-pypi-deps.mjs +90 -0
  33. package/skills/finding-agents/SKILL.md +6 -2
  34. package/skills/letta-guide/SKILL.md +21 -2
  35. package/skills/self-configuration/SKILL.md +11 -1
  36. package/skills/converting-mcps-to-skills/SKILL.md +0 -202
  37. package/skills/converting-mcps-to-skills/references/skill-templates.md +0 -141
  38. package/skills/converting-mcps-to-skills/scripts/mcp-http.ts +0 -980
  39. package/skills/converting-mcps-to-skills/scripts/mcp-stdio.ts +0 -494
@@ -1,980 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * MCP HTTP Client - Connect to any MCP server over HTTP
4
- *
5
- * Supports:
6
- * - Static bearer / custom headers (--header "Authorization: Bearer ...")
7
- * - OAuth 2.1 with PKCE + dynamic client registration (auto or --auth oauth)
8
- *
9
- * Usage:
10
- * npx tsx mcp-http.ts <url> [options] <command> [args]
11
- *
12
- * Commands:
13
- * list-tools List available tools
14
- * list-resources List available resources
15
- * info <tool> Show tool schema
16
- * call <tool> '<json>' Call a tool with JSON arguments
17
- * login Run OAuth flow and cache tokens for this server
18
- * logout Clear cached OAuth tokens for this server
19
- *
20
- * Options:
21
- * --header, -H "K: V" Add HTTP header (repeatable). Disables auto-OAuth.
22
- * --auth <mode> "auto" (default), "oauth", or "none"
23
- * --timeout <ms> Request timeout (default: 30000)
24
- * --help, -h Show this help
25
- */
26
-
27
- import { spawn } from "node:child_process";
28
- import * as crypto from "node:crypto";
29
- import * as fs from "node:fs";
30
- import * as http from "node:http";
31
- import * as os from "node:os";
32
- import * as path from "node:path";
33
- import { URL } from "node:url";
34
-
35
- // -------------------- Types --------------------
36
-
37
- interface JsonRpcRequest {
38
- jsonrpc: "2.0";
39
- method: string;
40
- params?: object;
41
- id: number;
42
- }
43
-
44
- interface JsonRpcResponse {
45
- jsonrpc: "2.0";
46
- result?: unknown;
47
- error?: { code: number; message: string; data?: unknown };
48
- id: number;
49
- }
50
-
51
- interface ParsedArgs {
52
- url: string;
53
- command: string;
54
- commandArgs: string[];
55
- headers: Record<string, string>;
56
- authMode: "auto" | "oauth" | "none";
57
- timeoutMs: number;
58
- }
59
-
60
- interface OAuthServerMetadata {
61
- issuer: string;
62
- authorization_endpoint: string;
63
- token_endpoint: string;
64
- registration_endpoint?: string;
65
- revocation_endpoint?: string;
66
- code_challenge_methods_supported?: string[];
67
- grant_types_supported?: string[];
68
- scopes_supported?: string[];
69
- }
70
-
71
- interface RegisteredClient {
72
- client_id: string;
73
- client_secret?: string;
74
- registration_access_token?: string;
75
- registration_client_uri?: string;
76
- redirect_uris: string[];
77
- token_endpoint_auth_method?: string;
78
- }
79
-
80
- interface TokenSet {
81
- access_token: string;
82
- refresh_token?: string;
83
- token_type: string;
84
- scope?: string;
85
- expires_at?: number; // epoch seconds
86
- }
87
-
88
- interface CachedAuth {
89
- server_url: string;
90
- authorization_server: string;
91
- metadata: OAuthServerMetadata;
92
- client: RegisteredClient;
93
- tokens?: TokenSet;
94
- }
95
-
96
- // -------------------- Arg parsing --------------------
97
-
98
- function parseArgs(): ParsedArgs {
99
- const args = process.argv.slice(2);
100
- const headers: Record<string, string> = {};
101
- let url = "";
102
- let command = "";
103
- const commandArgs: string[] = [];
104
- let authMode: "auto" | "oauth" | "none" = "auto";
105
- let timeoutMs = 30000;
106
-
107
- let i = 0;
108
- while (i < args.length) {
109
- const arg = args[i];
110
- if (!arg) {
111
- i++;
112
- continue;
113
- }
114
-
115
- if (arg === "--header" || arg === "-H") {
116
- const headerValue = args[++i];
117
- if (headerValue) {
118
- const colonIndex = headerValue.indexOf(":");
119
- if (colonIndex > 0) {
120
- const key = headerValue.slice(0, colonIndex).trim();
121
- const value = headerValue.slice(colonIndex + 1).trim();
122
- headers[key] = value;
123
- }
124
- }
125
- } else if (arg === "--auth") {
126
- const v = args[++i];
127
- if (v === "auto" || v === "oauth" || v === "none") authMode = v;
128
- } else if (arg === "--timeout") {
129
- const v = args[++i];
130
- if (v) timeoutMs = Number(v) || timeoutMs;
131
- } else if (arg === "--help" || arg === "-h") {
132
- printUsage();
133
- process.exit(0);
134
- } else if (!url && arg.startsWith("http")) {
135
- url = arg;
136
- } else if (!command) {
137
- command = arg;
138
- } else {
139
- commandArgs.push(arg);
140
- }
141
- i++;
142
- }
143
-
144
- return { url, command, commandArgs, headers, authMode, timeoutMs };
145
- }
146
-
147
- // -------------------- Token cache --------------------
148
-
149
- function cacheDir(): string {
150
- const dir = path.join(os.homedir(), ".letta", "mcp-oauth");
151
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
152
- return dir;
153
- }
154
-
155
- function cachePathForUrl(serverUrl: string): string {
156
- const u = new URL(serverUrl);
157
- // Include host + path so different MCP servers on the same host stay separate.
158
- const key = `${u.host}${u.pathname}`.replace(/[^a-zA-Z0-9._-]+/g, "_");
159
- return path.join(cacheDir(), `${key}.json`);
160
- }
161
-
162
- function loadCache(serverUrl: string): CachedAuth | null {
163
- const p = cachePathForUrl(serverUrl);
164
- try {
165
- const raw = fs.readFileSync(p, "utf8");
166
- return JSON.parse(raw) as CachedAuth;
167
- } catch {
168
- return null;
169
- }
170
- }
171
-
172
- function saveCache(serverUrl: string, data: CachedAuth): void {
173
- const p = cachePathForUrl(serverUrl);
174
- fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 0o600 });
175
- try {
176
- fs.chmodSync(p, 0o600);
177
- } catch {
178
- /* best effort */
179
- }
180
- }
181
-
182
- function clearCache(serverUrl: string): boolean {
183
- const p = cachePathForUrl(serverUrl);
184
- try {
185
- fs.unlinkSync(p);
186
- return true;
187
- } catch {
188
- return false;
189
- }
190
- }
191
-
192
- // -------------------- OAuth helpers --------------------
193
-
194
- function b64url(buf: Buffer): string {
195
- return buf
196
- .toString("base64")
197
- .replace(/\+/g, "-")
198
- .replace(/\//g, "_")
199
- .replace(/=+$/, "");
200
- }
201
-
202
- function makePkce(): { verifier: string; challenge: string } {
203
- const verifier = b64url(crypto.randomBytes(32));
204
- const challenge = b64url(
205
- crypto.createHash("sha256").update(verifier).digest(),
206
- );
207
- return { verifier, challenge };
208
- }
209
-
210
- async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
211
- const res = await fetch(url, init);
212
- const text = await res.text();
213
- if (!res.ok) {
214
- throw new Error(`HTTP ${res.status} from ${url}: ${text.slice(0, 400)}`);
215
- }
216
- try {
217
- return JSON.parse(text) as T;
218
- } catch {
219
- throw new Error(`Non-JSON response from ${url}: ${text.slice(0, 200)}`);
220
- }
221
- }
222
-
223
- /**
224
- * Parse `WWW-Authenticate: Bearer realm="...", resource_metadata="..."` etc.
225
- * Returns a flat map of params. Case-insensitive keys.
226
- */
227
- function parseWwwAuthenticate(header: string): Record<string, string> {
228
- const idx = header.indexOf(" ");
229
- const params = idx >= 0 ? header.slice(idx + 1) : header;
230
- const out: Record<string, string> = {};
231
- const re =
232
- /([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"|([a-zA-Z0-9_-]+)\s*=\s*([^,\s]+)/g;
233
- let m: RegExpExecArray | null;
234
- // biome-ignore lint/suspicious/noAssignInExpressions: standard regex.exec loop
235
- while ((m = re.exec(params))) {
236
- const key = (m[1] || m[3] || "").toLowerCase();
237
- const val = m[2] ?? m[4] ?? "";
238
- if (key) out[key] = val;
239
- }
240
- return out;
241
- }
242
-
243
- /**
244
- * Given an MCP server URL and a 401 WWW-Authenticate value, discover the OAuth
245
- * authorization server metadata. Prefers `resource_metadata` when present, then
246
- * falls back to `realm`, then to the server's own origin.
247
- */
248
- async function discoverAuthServer(
249
- serverUrl: string,
250
- wwwAuth: string | null,
251
- ): Promise<{ authServer: string; metadata: OAuthServerMetadata }> {
252
- const candidates: string[] = [];
253
- const params = wwwAuth ? parseWwwAuthenticate(wwwAuth) : {};
254
-
255
- if (params.resource_metadata) {
256
- try {
257
- const rm = await fetchJson<{ authorization_servers?: string[] }>(
258
- params.resource_metadata,
259
- );
260
- if (rm.authorization_servers?.length) {
261
- candidates.push(...rm.authorization_servers);
262
- }
263
- } catch {
264
- /* fall through */
265
- }
266
- }
267
- if (params.realm) candidates.push(params.realm);
268
- candidates.push(new URL(serverUrl).origin);
269
-
270
- const tried: string[] = [];
271
- for (const issuer of candidates) {
272
- const base = issuer.replace(/\/$/, "");
273
- const wellKnowns = [
274
- `${base}/.well-known/oauth-authorization-server`,
275
- `${base}/.well-known/openid-configuration`,
276
- ];
277
- for (const w of wellKnowns) {
278
- tried.push(w);
279
- try {
280
- const metadata = await fetchJson<OAuthServerMetadata>(w);
281
- if (metadata.authorization_endpoint && metadata.token_endpoint) {
282
- return { authServer: base, metadata };
283
- }
284
- } catch {
285
- /* try next */
286
- }
287
- }
288
- }
289
- throw new Error(
290
- `Could not discover OAuth server metadata. Tried:\n ${tried.join("\n ")}`,
291
- );
292
- }
293
-
294
- async function registerClient(
295
- metadata: OAuthServerMetadata,
296
- redirectUri: string,
297
- ): Promise<RegisteredClient> {
298
- if (!metadata.registration_endpoint) {
299
- throw new Error(
300
- `Auth server has no registration_endpoint. Manual client registration is required; ` +
301
- `pass a client_id via a static bearer header instead.`,
302
- );
303
- }
304
- const body = {
305
- client_name: "letta-code mcp-http",
306
- redirect_uris: [redirectUri],
307
- grant_types: ["authorization_code", "refresh_token"],
308
- response_types: ["code"],
309
- token_endpoint_auth_method: "none", // public client (PKCE)
310
- application_type: "native",
311
- };
312
- const res = await fetch(metadata.registration_endpoint, {
313
- method: "POST",
314
- headers: { "Content-Type": "application/json" },
315
- body: JSON.stringify(body),
316
- });
317
- const text = await res.text();
318
- if (!res.ok) {
319
- throw new Error(
320
- `Dynamic client registration failed (${res.status}): ${text.slice(0, 400)}`,
321
- );
322
- }
323
- const parsed = JSON.parse(text) as RegisteredClient;
324
- return {
325
- client_id: parsed.client_id,
326
- client_secret: parsed.client_secret,
327
- registration_access_token: parsed.registration_access_token,
328
- registration_client_uri: parsed.registration_client_uri,
329
- redirect_uris: parsed.redirect_uris || [redirectUri],
330
- token_endpoint_auth_method: parsed.token_endpoint_auth_method || "none",
331
- };
332
- }
333
-
334
- function openBrowser(url: string): void {
335
- const platform = process.platform;
336
- const cmd =
337
- platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
338
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
339
- try {
340
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
341
- } catch {
342
- /* user can copy the URL from stderr */
343
- }
344
- }
345
-
346
- interface LoopbackResult {
347
- code: string;
348
- state: string;
349
- }
350
-
351
- interface LoopbackListener {
352
- port: number;
353
- done: Promise<LoopbackResult>;
354
- }
355
-
356
- function startLoopback(expectedState: string): Promise<LoopbackListener> {
357
- return new Promise((resolveOuter, rejectOuter) => {
358
- let resolveDone!: (r: LoopbackResult) => void;
359
- let rejectDone!: (e: Error) => void;
360
- const done = new Promise<LoopbackResult>((res, rej) => {
361
- resolveDone = res;
362
- rejectDone = rej;
363
- });
364
- const server = http.createServer((req, res) => {
365
- try {
366
- const reqUrl = new URL(req.url || "/", "http://127.0.0.1");
367
- if (reqUrl.pathname !== "/callback") {
368
- res.writeHead(404).end("not found");
369
- return;
370
- }
371
- const code = reqUrl.searchParams.get("code");
372
- const state = reqUrl.searchParams.get("state");
373
- const error = reqUrl.searchParams.get("error");
374
- if (error) {
375
- res.writeHead(400, { "Content-Type": "text/html" });
376
- res.end(`<h1>Authorization failed</h1><p>${error}</p>`);
377
- server.close();
378
- rejectDone(new Error(`Authorization error: ${error}`));
379
- return;
380
- }
381
- if (!code || !state) {
382
- res.writeHead(400).end("missing code or state");
383
- return;
384
- }
385
- if (state !== expectedState) {
386
- res.writeHead(400).end("state mismatch");
387
- server.close();
388
- rejectDone(new Error("OAuth state mismatch"));
389
- return;
390
- }
391
- res.writeHead(200, { "Content-Type": "text/html" });
392
- res.end(
393
- `<!doctype html><html><body style="font-family:system-ui;padding:2rem"><h1>Signed in.</h1><p>You can close this tab and return to the terminal.</p></body></html>`,
394
- );
395
- server.close();
396
- resolveDone({ code, state });
397
- } catch (e) {
398
- rejectDone(e as Error);
399
- }
400
- });
401
- server.on("error", (e) => {
402
- rejectDone(e);
403
- rejectOuter(e);
404
- });
405
- server.listen(0, "127.0.0.1", () => {
406
- const port = (server.address() as { port: number }).port;
407
- resolveOuter({ port, done });
408
- });
409
- });
410
- }
411
-
412
- async function exchangeCodeForTokens(
413
- metadata: OAuthServerMetadata,
414
- client: RegisteredClient,
415
- code: string,
416
- verifier: string,
417
- redirectUri: string,
418
- resource: string,
419
- ): Promise<TokenSet> {
420
- const body = new URLSearchParams({
421
- grant_type: "authorization_code",
422
- code,
423
- redirect_uri: redirectUri,
424
- client_id: client.client_id,
425
- code_verifier: verifier,
426
- resource,
427
- });
428
- if (client.client_secret) body.set("client_secret", client.client_secret);
429
-
430
- const res = await fetch(metadata.token_endpoint, {
431
- method: "POST",
432
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
433
- body: body.toString(),
434
- });
435
- const text = await res.text();
436
- if (!res.ok) {
437
- throw new Error(
438
- `Token exchange failed (${res.status}): ${text.slice(0, 400)}`,
439
- );
440
- }
441
- const parsed = JSON.parse(text) as {
442
- access_token: string;
443
- refresh_token?: string;
444
- token_type: string;
445
- expires_in?: number;
446
- scope?: string;
447
- };
448
- const now = Math.floor(Date.now() / 1000);
449
- return {
450
- access_token: parsed.access_token,
451
- refresh_token: parsed.refresh_token,
452
- token_type: parsed.token_type || "Bearer",
453
- scope: parsed.scope,
454
- expires_at: parsed.expires_in ? now + parsed.expires_in - 30 : undefined,
455
- };
456
- }
457
-
458
- async function refreshTokens(
459
- metadata: OAuthServerMetadata,
460
- client: RegisteredClient,
461
- refreshToken: string,
462
- resource: string,
463
- ): Promise<TokenSet> {
464
- const body = new URLSearchParams({
465
- grant_type: "refresh_token",
466
- refresh_token: refreshToken,
467
- client_id: client.client_id,
468
- resource,
469
- });
470
- if (client.client_secret) body.set("client_secret", client.client_secret);
471
- const res = await fetch(metadata.token_endpoint, {
472
- method: "POST",
473
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
474
- body: body.toString(),
475
- });
476
- const text = await res.text();
477
- if (!res.ok) {
478
- throw new Error(
479
- `Token refresh failed (${res.status}): ${text.slice(0, 400)}`,
480
- );
481
- }
482
- const parsed = JSON.parse(text) as {
483
- access_token: string;
484
- refresh_token?: string;
485
- token_type: string;
486
- expires_in?: number;
487
- scope?: string;
488
- };
489
- const now = Math.floor(Date.now() / 1000);
490
- return {
491
- access_token: parsed.access_token,
492
- refresh_token: parsed.refresh_token || refreshToken,
493
- token_type: parsed.token_type || "Bearer",
494
- scope: parsed.scope,
495
- expires_at: parsed.expires_in ? now + parsed.expires_in - 30 : undefined,
496
- };
497
- }
498
-
499
- async function runOAuthLogin(
500
- serverUrl: string,
501
- wwwAuth: string | null,
502
- ): Promise<CachedAuth> {
503
- process.stderr.write(`Discovering OAuth for ${serverUrl}...\n`);
504
- const { authServer, metadata } = await discoverAuthServer(serverUrl, wwwAuth);
505
- process.stderr.write(`Authorization server: ${authServer}\n`);
506
-
507
- const state = b64url(crypto.randomBytes(16));
508
- const { verifier, challenge } = makePkce();
509
-
510
- const listener = await startLoopback(state);
511
- const redirectUri = `http://127.0.0.1:${listener.port}/callback`;
512
-
513
- process.stderr.write(`Registering client dynamically...\n`);
514
- const client = await registerClient(metadata, redirectUri);
515
-
516
- const authUrl = new URL(metadata.authorization_endpoint);
517
- authUrl.searchParams.set("response_type", "code");
518
- authUrl.searchParams.set("client_id", client.client_id);
519
- authUrl.searchParams.set("redirect_uri", redirectUri);
520
- authUrl.searchParams.set("state", state);
521
- authUrl.searchParams.set("code_challenge", challenge);
522
- authUrl.searchParams.set("code_challenge_method", "S256");
523
- authUrl.searchParams.set("resource", serverUrl);
524
- const scopes = metadata.scopes_supported?.includes("offline_access")
525
- ? "openid offline_access"
526
- : "openid";
527
- authUrl.searchParams.set("scope", scopes);
528
-
529
- process.stderr.write(
530
- `Opening browser to sign in:\n ${authUrl.toString()}\n`,
531
- );
532
- openBrowser(authUrl.toString());
533
-
534
- const result = await listener.done;
535
- process.stderr.write(`Exchanging code for tokens...\n`);
536
- const tokens = await exchangeCodeForTokens(
537
- metadata,
538
- client,
539
- result.code,
540
- verifier,
541
- redirectUri,
542
- serverUrl,
543
- );
544
-
545
- const cache: CachedAuth = {
546
- server_url: serverUrl,
547
- authorization_server: authServer,
548
- metadata,
549
- client,
550
- tokens,
551
- };
552
- saveCache(serverUrl, cache);
553
- process.stderr.write(
554
- `Signed in. Tokens cached at ${cachePathForUrl(serverUrl)}\n`,
555
- );
556
- return cache;
557
- }
558
-
559
- async function ensureAccessToken(cache: CachedAuth): Promise<string> {
560
- const now = Math.floor(Date.now() / 1000);
561
- const tokens = cache.tokens;
562
- if (!tokens) throw new Error("No tokens in cache; run `login`.");
563
- if (!tokens.expires_at || tokens.expires_at > now) {
564
- return tokens.access_token;
565
- }
566
- if (!tokens.refresh_token) {
567
- throw new Error(
568
- "Access token expired and no refresh token; run `login` again.",
569
- );
570
- }
571
- const fresh = await refreshTokens(
572
- cache.metadata,
573
- cache.client,
574
- tokens.refresh_token,
575
- cache.server_url,
576
- );
577
- cache.tokens = fresh;
578
- saveCache(cache.server_url, cache);
579
- return fresh.access_token;
580
- }
581
-
582
- // -------------------- MCP session --------------------
583
-
584
- let sessionId: string | null = null;
585
- let initialized = false;
586
- let requestHeaders: Record<string, string> = {};
587
- let serverUrl = "";
588
- let timeoutMs = 30000;
589
- let authMode: "auto" | "oauth" | "none" = "auto";
590
- let currentAuth: CachedAuth | null = null;
591
-
592
- async function fetchWithTimeout(
593
- url: string,
594
- init: RequestInit,
595
- ): Promise<Response> {
596
- const ctrl = new AbortController();
597
- const timer = setTimeout(() => ctrl.abort(), timeoutMs);
598
- try {
599
- return await fetch(url, { ...init, signal: ctrl.signal });
600
- } finally {
601
- clearTimeout(timer);
602
- }
603
- }
604
-
605
- async function rawMcpRequest(
606
- method: string,
607
- params?: object,
608
- opts: { allowOAuth?: boolean } = {},
609
- ): Promise<{ response: JsonRpcResponse; newSessionId?: string }> {
610
- const request: JsonRpcRequest = {
611
- jsonrpc: "2.0",
612
- method,
613
- params,
614
- id: Date.now(),
615
- };
616
-
617
- const headers: Record<string, string> = {
618
- "Content-Type": "application/json",
619
- Accept: "application/json, text/event-stream",
620
- ...requestHeaders,
621
- };
622
-
623
- // If we have an OAuth token and no explicit Authorization header, attach it.
624
- if (!headers.Authorization && currentAuth?.tokens) {
625
- const access = await ensureAccessToken(currentAuth);
626
- headers.Authorization = `Bearer ${access}`;
627
- }
628
-
629
- if (sessionId) headers["Mcp-Session-Id"] = sessionId;
630
-
631
- let fetchResponse: Response;
632
- try {
633
- fetchResponse = await fetchWithTimeout(serverUrl, {
634
- method: "POST",
635
- headers,
636
- body: JSON.stringify(request),
637
- });
638
- } catch (error) {
639
- if (error instanceof TypeError && error.message.includes("fetch")) {
640
- throw new Error(
641
- `Cannot connect to ${serverUrl}\nIs the MCP server running?`,
642
- );
643
- }
644
- throw error;
645
- }
646
-
647
- const newSessionId = fetchResponse.headers.get("Mcp-Session-Id") || undefined;
648
-
649
- if (fetchResponse.status === 401) {
650
- const wwwAuth = fetchResponse.headers.get("www-authenticate");
651
- const hasStaticAuth = Boolean(requestHeaders.Authorization);
652
- const canOAuth =
653
- opts.allowOAuth !== false &&
654
- !hasStaticAuth &&
655
- (authMode === "oauth" ||
656
- (authMode === "auto" && wwwAuth?.toLowerCase().startsWith("bearer")));
657
-
658
- if (canOAuth) {
659
- await fetchResponse.text().catch(() => "");
660
- if (!currentAuth) currentAuth = loadCache(serverUrl);
661
- if (currentAuth?.tokens?.refresh_token) {
662
- try {
663
- currentAuth.tokens = await refreshTokens(
664
- currentAuth.metadata,
665
- currentAuth.client,
666
- currentAuth.tokens.refresh_token,
667
- serverUrl,
668
- );
669
- saveCache(serverUrl, currentAuth);
670
- } catch {
671
- currentAuth = await runOAuthLogin(serverUrl, wwwAuth);
672
- }
673
- } else {
674
- currentAuth = await runOAuthLogin(serverUrl, wwwAuth);
675
- }
676
- return rawMcpRequest(method, params, { allowOAuth: false });
677
- }
678
-
679
- const text = await fetchResponse.text().catch(() => "");
680
- throw new Error(
681
- `Authentication required (401).${
682
- wwwAuth ? ` WWW-Authenticate: ${wwwAuth}` : ""
683
- }\n${text}\n` +
684
- `Add --header "Authorization: Bearer YOUR_KEY", or run \`login\` first, or pass --auth oauth.`,
685
- );
686
- }
687
-
688
- if (!fetchResponse.ok) {
689
- const text = await fetchResponse.text();
690
- try {
691
- const errorResponse = JSON.parse(text) as JsonRpcResponse;
692
- return { response: errorResponse, newSessionId };
693
- } catch {
694
- throw new Error(
695
- `HTTP ${fetchResponse.status}: ${fetchResponse.statusText}\n${text}`,
696
- );
697
- }
698
- }
699
-
700
- const contentType = fetchResponse.headers.get("content-type") || "";
701
-
702
- if (contentType.includes("application/json")) {
703
- const jsonResponse = (await fetchResponse.json()) as JsonRpcResponse;
704
- return { response: jsonResponse, newSessionId };
705
- }
706
-
707
- if (contentType.includes("text/event-stream")) {
708
- const text = await fetchResponse.text();
709
- const dataLines = text
710
- .split("\n")
711
- .filter((line) => line.startsWith("data: "))
712
- .map((line) => line.slice(6));
713
-
714
- for (let i = dataLines.length - 1; i >= 0; i--) {
715
- const line = dataLines[i];
716
- if (!line) continue;
717
- try {
718
- const parsed = JSON.parse(line);
719
- if (parsed.jsonrpc === "2.0") {
720
- return { response: parsed as JsonRpcResponse, newSessionId };
721
- }
722
- } catch {
723
- /* continue */
724
- }
725
- }
726
- throw new Error("No valid JSON-RPC response found in SSE stream");
727
- }
728
-
729
- throw new Error(`Unexpected content type: ${contentType}`);
730
- }
731
-
732
- async function ensureInitialized(): Promise<void> {
733
- if (initialized) return;
734
- const { response, newSessionId } = await rawMcpRequest("initialize", {
735
- protocolVersion: "2024-11-05",
736
- capabilities: {},
737
- clientInfo: { name: "mcp-http-cli", version: "1.1.0" },
738
- });
739
- if (newSessionId) sessionId = newSessionId;
740
- if (response.error) {
741
- throw new Error(`Initialization failed: ${response.error.message}`);
742
- }
743
- await rawMcpRequest("notifications/initialized", {});
744
- initialized = true;
745
- }
746
-
747
- async function mcpRequest(
748
- method: string,
749
- params?: object,
750
- ): Promise<JsonRpcResponse> {
751
- await ensureInitialized();
752
- const { response, newSessionId } = await rawMcpRequest(method, params);
753
- if (newSessionId) sessionId = newSessionId;
754
- return response;
755
- }
756
-
757
- // -------------------- Commands --------------------
758
-
759
- async function listTools(): Promise<void> {
760
- const response = await mcpRequest("tools/list");
761
- if (response.error) {
762
- console.error("Error:", response.error.message);
763
- process.exit(1);
764
- }
765
- const result = response.result as {
766
- tools: Array<{ name: string; description: string; inputSchema: object }>;
767
- };
768
- console.log("Available tools:\n");
769
- for (const tool of result.tools) {
770
- console.log(` ${tool.name}`);
771
- if (tool.description) console.log(` ${tool.description}\n`);
772
- else console.log();
773
- }
774
- console.log(`\nTotal: ${result.tools.length} tools`);
775
- console.log("\nUse 'call <tool> <json-args>' to invoke a tool");
776
- }
777
-
778
- async function listResources(): Promise<void> {
779
- const response = await mcpRequest("resources/list");
780
- if (response.error) {
781
- console.error("Error:", response.error.message);
782
- process.exit(1);
783
- }
784
- const result = response.result as {
785
- resources: Array<{ uri: string; name: string; description?: string }>;
786
- };
787
- if (!result.resources || result.resources.length === 0) {
788
- console.log("No resources available.");
789
- return;
790
- }
791
- console.log("Available resources:\n");
792
- for (const resource of result.resources) {
793
- console.log(` ${resource.uri}`);
794
- console.log(` ${resource.name}`);
795
- if (resource.description) console.log(` ${resource.description}`);
796
- console.log();
797
- }
798
- }
799
-
800
- async function callTool(toolName: string, argsJson: string): Promise<void> {
801
- let args: object;
802
- try {
803
- args = JSON.parse(argsJson || "{}");
804
- } catch {
805
- console.error(`Invalid JSON: ${argsJson}`);
806
- process.exit(1);
807
- }
808
- const response = await mcpRequest("tools/call", {
809
- name: toolName,
810
- arguments: args,
811
- });
812
- if (response.error) {
813
- console.error("Error:", response.error.message);
814
- if (response.error.data) {
815
- console.error("Details:", JSON.stringify(response.error.data, null, 2));
816
- }
817
- process.exit(1);
818
- }
819
- console.log(JSON.stringify(response.result, null, 2));
820
- }
821
-
822
- async function getToolSchema(toolName: string): Promise<void> {
823
- const response = await mcpRequest("tools/list");
824
- if (response.error) {
825
- console.error("Error:", response.error.message);
826
- process.exit(1);
827
- }
828
- const result = response.result as {
829
- tools: Array<{ name: string; description: string; inputSchema: object }>;
830
- };
831
- const tool = result.tools.find((t) => t.name === toolName);
832
- if (!tool) {
833
- console.error(`Tool not found: ${toolName}`);
834
- console.error(
835
- `Available tools: ${result.tools.map((t) => t.name).join(", ")}`,
836
- );
837
- process.exit(1);
838
- }
839
- console.log(`Tool: ${tool.name}\n`);
840
- if (tool.description) console.log(`Description: ${tool.description}\n`);
841
- console.log("Input Schema:");
842
- console.log(JSON.stringify(tool.inputSchema, null, 2));
843
- }
844
-
845
- async function loginCommand(): Promise<void> {
846
- let wwwAuth: string | null = null;
847
- try {
848
- const res = await fetchWithTimeout(serverUrl, {
849
- method: "POST",
850
- headers: {
851
- "Content-Type": "application/json",
852
- Accept: "application/json, text/event-stream",
853
- },
854
- body: JSON.stringify({
855
- jsonrpc: "2.0",
856
- method: "initialize",
857
- params: {
858
- protocolVersion: "2024-11-05",
859
- capabilities: {},
860
- clientInfo: { name: "mcp-http-cli", version: "1.1.0" },
861
- },
862
- id: 1,
863
- }),
864
- });
865
- wwwAuth = res.headers.get("www-authenticate");
866
- } catch {
867
- /* proceed anyway */
868
- }
869
- await runOAuthLogin(serverUrl, wwwAuth);
870
- }
871
-
872
- function logoutCommand(): void {
873
- const removed = clearCache(serverUrl);
874
- console.log(
875
- removed
876
- ? `Cleared cached tokens for ${serverUrl}`
877
- : `No cached tokens for ${serverUrl}`,
878
- );
879
- }
880
-
881
- function printUsage(): void {
882
- console.log(`MCP HTTP Client - Connect to any MCP server over HTTP
883
-
884
- Usage: npx tsx mcp-http.ts <url> [options] <command> [args]
885
-
886
- Commands:
887
- list-tools List available tools with descriptions
888
- list-resources List available resources
889
- info <tool> Show tool schema/parameters
890
- call <tool> '<json>' Call a tool with JSON arguments
891
- login Run OAuth flow and cache tokens for this server
892
- logout Clear cached OAuth tokens for this server
893
-
894
- Options:
895
- --header, -H "K: V" Add HTTP header (repeatable). Disables auto-OAuth.
896
- --auth <mode> "auto" (default), "oauth", or "none"
897
- --timeout <ms> Request timeout (default: 30000)
898
- --help, -h Show this help
899
-
900
- Authentication:
901
- Static bearer: --header "Authorization: Bearer YOUR_KEY"
902
- OAuth 2.1: Just run any command; a 401 with WWW-Authenticate triggers
903
- browser-based login via PKCE + dynamic client registration.
904
- Tokens are cached at ~/.letta/mcp-oauth/ and auto-refreshed.
905
-
906
- Examples:
907
- npx tsx mcp-http.ts http://localhost:3001/mcp list-tools
908
- npx tsx mcp-http.ts https://example.com/mcp login
909
- npx tsx mcp-http.ts https://example.com/mcp list-tools
910
- npx tsx mcp-http.ts https://example.com/mcp call some_tool '{"x":1}'
911
- `);
912
- }
913
-
914
- async function main(): Promise<void> {
915
- const parsed = parseArgs();
916
-
917
- if (!parsed.url) {
918
- console.error("Error: URL is required\n");
919
- printUsage();
920
- process.exit(1);
921
- }
922
-
923
- serverUrl = parsed.url;
924
- requestHeaders = parsed.headers;
925
- timeoutMs = parsed.timeoutMs;
926
- authMode = parsed.authMode;
927
- currentAuth = authMode === "none" ? null : loadCache(serverUrl);
928
-
929
- if (!parsed.command) {
930
- console.error("Error: Command is required\n");
931
- printUsage();
932
- process.exit(1);
933
- }
934
-
935
- try {
936
- switch (parsed.command) {
937
- case "list-tools":
938
- await listTools();
939
- break;
940
- case "list-resources":
941
- await listResources();
942
- break;
943
- case "info": {
944
- const [toolName] = parsed.commandArgs;
945
- if (!toolName) {
946
- console.error("Error: Tool name required\nUsage: info <tool>");
947
- process.exit(1);
948
- }
949
- await getToolSchema(toolName);
950
- break;
951
- }
952
- case "call": {
953
- const [toolName, argsJson] = parsed.commandArgs;
954
- if (!toolName) {
955
- console.error(
956
- "Error: Tool name required\nUsage: call <tool> '<json-args>'",
957
- );
958
- process.exit(1);
959
- }
960
- await callTool(toolName, argsJson || "{}");
961
- break;
962
- }
963
- case "login":
964
- await loginCommand();
965
- break;
966
- case "logout":
967
- logoutCommand();
968
- break;
969
- default:
970
- console.error(`Unknown command: ${parsed.command}\n`);
971
- printUsage();
972
- process.exit(1);
973
- }
974
- } catch (error) {
975
- console.error("Error:", error instanceof Error ? error.message : error);
976
- process.exit(1);
977
- }
978
- }
979
-
980
- main();