@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.
- package/dist/channels-slack.js +135 -54
- package/dist/channels-slack.js.map +3 -3
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/message.d.ts.map +1 -1
- package/dist/types/agent/subagents/manager.d.ts.map +1 -1
- package/dist/types/agent/subagents/subagent-process.d.ts +23 -0
- package/dist/types/agent/subagents/subagent-process.d.ts.map +1 -0
- package/dist/types/backend/api/metadata.d.ts +12 -0
- package/dist/types/backend/api/metadata.d.ts.map +1 -1
- package/dist/types/channels/slack/status-controller.d.ts +2 -0
- package/dist/types/channels/slack/status-controller.d.ts.map +1 -1
- package/dist/types/cli/helpers/error-formatter.d.ts.map +1 -1
- package/dist/types/cli/helpers/stream-processor.d.ts +2 -0
- package/dist/types/cli/helpers/stream-processor.d.ts.map +1 -1
- package/dist/types/telemetry/index.d.ts +16 -1
- package/dist/types/telemetry/index.d.ts.map +1 -1
- package/dist/types/tools/impl/process_manager.d.ts +2 -0
- package/dist/types/tools/impl/process_manager.d.ts.map +1 -1
- package/dist/types/tools/impl/task-stop.d.ts.map +1 -1
- package/dist/types/tools/impl/task.d.ts.map +1 -1
- package/dist/types/utils/cloud-api-shutdown.d.ts +17 -0
- package/dist/types/utils/cloud-api-shutdown.d.ts.map +1 -0
- package/dist/types/websocket/listener/constants.d.ts +1 -0
- package/dist/types/websocket/listener/constants.d.ts.map +1 -1
- package/letta.js +2 -2
- package/package.json +1 -1
- package/scripts/build-pypi.py +172 -0
- package/scripts/isolated-unit-tests.json +5 -0
- package/scripts/smoke-pypi.py +236 -0
- package/scripts/source-file-size-baseline.json +1 -1
- package/scripts/stage-pypi-deps.mjs +90 -0
- package/skills/finding-agents/SKILL.md +6 -2
- package/skills/letta-guide/SKILL.md +21 -2
- package/skills/self-configuration/SKILL.md +11 -1
- package/skills/converting-mcps-to-skills/SKILL.md +0 -202
- package/skills/converting-mcps-to-skills/references/skill-templates.md +0 -141
- package/skills/converting-mcps-to-skills/scripts/mcp-http.ts +0 -980
- 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();
|