@letta-ai/letta-code 0.28.7 → 0.28.8
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/agent-presets.js +80 -1
- package/dist/agent-presets.js.map +2 -2
- package/letta.js +467 -239
- package/package.json +1 -1
- package/scripts/source-file-size-baseline.json +2 -2
- package/skills/converting-mcps-to-skills/SKILL.md +44 -3
- package/skills/converting-mcps-to-skills/scripts/mcp-http.ts +696 -145
|
@@ -2,23 +2,38 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MCP HTTP Client - Connect to any MCP server over HTTP
|
|
4
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
|
+
*
|
|
5
9
|
* Usage:
|
|
6
|
-
* npx tsx mcp-http.ts <url> <command> [args]
|
|
10
|
+
* npx tsx mcp-http.ts <url> [options] <command> [args]
|
|
7
11
|
*
|
|
8
12
|
* Commands:
|
|
9
13
|
* list-tools List available tools
|
|
10
14
|
* list-resources List available resources
|
|
15
|
+
* info <tool> Show tool schema
|
|
11
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
|
|
12
19
|
*
|
|
13
20
|
* Options:
|
|
14
|
-
* --header "
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
|
20
25
|
*/
|
|
21
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
|
+
|
|
22
37
|
interface JsonRpcRequest {
|
|
23
38
|
jsonrpc: "2.0";
|
|
24
39
|
method: string;
|
|
@@ -29,11 +44,7 @@ interface JsonRpcRequest {
|
|
|
29
44
|
interface JsonRpcResponse {
|
|
30
45
|
jsonrpc: "2.0";
|
|
31
46
|
result?: unknown;
|
|
32
|
-
error?: {
|
|
33
|
-
code: number;
|
|
34
|
-
message: string;
|
|
35
|
-
data?: unknown;
|
|
36
|
-
};
|
|
47
|
+
error?: { code: number; message: string; data?: unknown };
|
|
37
48
|
id: number;
|
|
38
49
|
}
|
|
39
50
|
|
|
@@ -42,14 +53,56 @@ interface ParsedArgs {
|
|
|
42
53
|
command: string;
|
|
43
54
|
commandArgs: string[];
|
|
44
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[];
|
|
45
69
|
}
|
|
46
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
|
+
|
|
47
98
|
function parseArgs(): ParsedArgs {
|
|
48
99
|
const args = process.argv.slice(2);
|
|
49
100
|
const headers: Record<string, string> = {};
|
|
50
101
|
let url = "";
|
|
51
102
|
let command = "";
|
|
52
103
|
const commandArgs: string[] = [];
|
|
104
|
+
let authMode: "auto" | "oauth" | "none" = "auto";
|
|
105
|
+
let timeoutMs = 30000;
|
|
53
106
|
|
|
54
107
|
let i = 0;
|
|
55
108
|
while (i < args.length) {
|
|
@@ -69,6 +122,12 @@ function parseArgs(): ParsedArgs {
|
|
|
69
122
|
headers[key] = value;
|
|
70
123
|
}
|
|
71
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;
|
|
72
131
|
} else if (arg === "--help" || arg === "-h") {
|
|
73
132
|
printUsage();
|
|
74
133
|
process.exit(0);
|
|
@@ -82,18 +141,471 @@ function parseArgs(): ParsedArgs {
|
|
|
82
141
|
i++;
|
|
83
142
|
}
|
|
84
143
|
|
|
85
|
-
return { url, command, commandArgs, headers };
|
|
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>;
|
|
86
354
|
}
|
|
87
355
|
|
|
88
|
-
|
|
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
|
+
|
|
89
584
|
let sessionId: string | null = null;
|
|
90
585
|
let initialized = false;
|
|
91
586
|
let requestHeaders: Record<string, string> = {};
|
|
92
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
|
+
}
|
|
93
604
|
|
|
94
605
|
async function rawMcpRequest(
|
|
95
606
|
method: string,
|
|
96
607
|
params?: object,
|
|
608
|
+
opts: { allowOAuth?: boolean } = {},
|
|
97
609
|
): Promise<{ response: JsonRpcResponse; newSessionId?: string }> {
|
|
98
610
|
const request: JsonRpcRequest = {
|
|
99
611
|
jsonrpc: "2.0",
|
|
@@ -108,106 +620,127 @@ async function rawMcpRequest(
|
|
|
108
620
|
...requestHeaders,
|
|
109
621
|
};
|
|
110
622
|
|
|
111
|
-
|
|
112
|
-
|
|
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}`;
|
|
113
627
|
}
|
|
114
628
|
|
|
629
|
+
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
|
630
|
+
|
|
631
|
+
let fetchResponse: Response;
|
|
115
632
|
try {
|
|
116
|
-
|
|
633
|
+
fetchResponse = await fetchWithTimeout(serverUrl, {
|
|
117
634
|
method: "POST",
|
|
118
635
|
headers,
|
|
119
636
|
body: JSON.stringify(request),
|
|
120
637
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 };
|
|
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
|
+
);
|
|
152
643
|
}
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
153
646
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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) {
|
|
165
662
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
663
|
+
currentAuth.tokens = await refreshTokens(
|
|
664
|
+
currentAuth.metadata,
|
|
665
|
+
currentAuth.client,
|
|
666
|
+
currentAuth.tokens.refresh_token,
|
|
667
|
+
serverUrl,
|
|
668
|
+
);
|
|
669
|
+
saveCache(serverUrl, currentAuth);
|
|
170
670
|
} catch {
|
|
171
|
-
|
|
671
|
+
currentAuth = await runOAuthLogin(serverUrl, wwwAuth);
|
|
172
672
|
}
|
|
673
|
+
} else {
|
|
674
|
+
currentAuth = await runOAuthLogin(serverUrl, wwwAuth);
|
|
173
675
|
}
|
|
174
|
-
|
|
676
|
+
return rawMcpRequest(method, params, { allowOAuth: false });
|
|
175
677
|
}
|
|
176
678
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 {
|
|
180
694
|
throw new Error(
|
|
181
|
-
`
|
|
695
|
+
`HTTP ${fetchResponse.status}: ${fetchResponse.statusText}\n${text}`,
|
|
182
696
|
);
|
|
183
697
|
}
|
|
184
|
-
throw error;
|
|
185
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}`);
|
|
186
730
|
}
|
|
187
731
|
|
|
188
732
|
async function ensureInitialized(): Promise<void> {
|
|
189
733
|
if (initialized) return;
|
|
190
|
-
|
|
191
734
|
const { response, newSessionId } = await rawMcpRequest("initialize", {
|
|
192
735
|
protocolVersion: "2024-11-05",
|
|
193
736
|
capabilities: {},
|
|
194
|
-
clientInfo: {
|
|
195
|
-
name: "mcp-http-cli",
|
|
196
|
-
version: "1.0.0",
|
|
197
|
-
},
|
|
737
|
+
clientInfo: { name: "mcp-http-cli", version: "1.1.0" },
|
|
198
738
|
});
|
|
199
|
-
|
|
200
|
-
if (newSessionId) {
|
|
201
|
-
sessionId = newSessionId;
|
|
202
|
-
}
|
|
203
|
-
|
|
739
|
+
if (newSessionId) sessionId = newSessionId;
|
|
204
740
|
if (response.error) {
|
|
205
741
|
throw new Error(`Initialization failed: ${response.error.message}`);
|
|
206
742
|
}
|
|
207
|
-
|
|
208
|
-
// Send initialized notification
|
|
209
743
|
await rawMcpRequest("notifications/initialized", {});
|
|
210
|
-
|
|
211
744
|
initialized = true;
|
|
212
745
|
}
|
|
213
746
|
|
|
@@ -216,66 +749,50 @@ async function mcpRequest(
|
|
|
216
749
|
params?: object,
|
|
217
750
|
): Promise<JsonRpcResponse> {
|
|
218
751
|
await ensureInitialized();
|
|
219
|
-
|
|
220
752
|
const { response, newSessionId } = await rawMcpRequest(method, params);
|
|
221
|
-
|
|
222
|
-
if (newSessionId) {
|
|
223
|
-
sessionId = newSessionId;
|
|
224
|
-
}
|
|
225
|
-
|
|
753
|
+
if (newSessionId) sessionId = newSessionId;
|
|
226
754
|
return response;
|
|
227
755
|
}
|
|
228
756
|
|
|
757
|
+
// -------------------- Commands --------------------
|
|
758
|
+
|
|
229
759
|
async function listTools(): Promise<void> {
|
|
230
760
|
const response = await mcpRequest("tools/list");
|
|
231
|
-
|
|
232
761
|
if (response.error) {
|
|
233
762
|
console.error("Error:", response.error.message);
|
|
234
763
|
process.exit(1);
|
|
235
764
|
}
|
|
236
|
-
|
|
237
765
|
const result = response.result as {
|
|
238
766
|
tools: Array<{ name: string; description: string; inputSchema: object }>;
|
|
239
767
|
};
|
|
240
|
-
|
|
241
768
|
console.log("Available tools:\n");
|
|
242
769
|
for (const tool of result.tools) {
|
|
243
770
|
console.log(` ${tool.name}`);
|
|
244
|
-
if (tool.description) {
|
|
245
|
-
|
|
246
|
-
} else {
|
|
247
|
-
console.log();
|
|
248
|
-
}
|
|
771
|
+
if (tool.description) console.log(` ${tool.description}\n`);
|
|
772
|
+
else console.log();
|
|
249
773
|
}
|
|
250
|
-
|
|
251
774
|
console.log(`\nTotal: ${result.tools.length} tools`);
|
|
252
775
|
console.log("\nUse 'call <tool> <json-args>' to invoke a tool");
|
|
253
776
|
}
|
|
254
777
|
|
|
255
778
|
async function listResources(): Promise<void> {
|
|
256
779
|
const response = await mcpRequest("resources/list");
|
|
257
|
-
|
|
258
780
|
if (response.error) {
|
|
259
781
|
console.error("Error:", response.error.message);
|
|
260
782
|
process.exit(1);
|
|
261
783
|
}
|
|
262
|
-
|
|
263
784
|
const result = response.result as {
|
|
264
785
|
resources: Array<{ uri: string; name: string; description?: string }>;
|
|
265
786
|
};
|
|
266
|
-
|
|
267
787
|
if (!result.resources || result.resources.length === 0) {
|
|
268
788
|
console.log("No resources available.");
|
|
269
789
|
return;
|
|
270
790
|
}
|
|
271
|
-
|
|
272
791
|
console.log("Available resources:\n");
|
|
273
792
|
for (const resource of result.resources) {
|
|
274
793
|
console.log(` ${resource.uri}`);
|
|
275
794
|
console.log(` ${resource.name}`);
|
|
276
|
-
if (resource.description) {
|
|
277
|
-
console.log(` ${resource.description}`);
|
|
278
|
-
}
|
|
795
|
+
if (resource.description) console.log(` ${resource.description}`);
|
|
279
796
|
console.log();
|
|
280
797
|
}
|
|
281
798
|
}
|
|
@@ -288,12 +805,10 @@ async function callTool(toolName: string, argsJson: string): Promise<void> {
|
|
|
288
805
|
console.error(`Invalid JSON: ${argsJson}`);
|
|
289
806
|
process.exit(1);
|
|
290
807
|
}
|
|
291
|
-
|
|
292
808
|
const response = await mcpRequest("tools/call", {
|
|
293
809
|
name: toolName,
|
|
294
810
|
arguments: args,
|
|
295
811
|
});
|
|
296
|
-
|
|
297
812
|
if (response.error) {
|
|
298
813
|
console.error("Error:", response.error.message);
|
|
299
814
|
if (response.error.data) {
|
|
@@ -301,22 +816,18 @@ async function callTool(toolName: string, argsJson: string): Promise<void> {
|
|
|
301
816
|
}
|
|
302
817
|
process.exit(1);
|
|
303
818
|
}
|
|
304
|
-
|
|
305
819
|
console.log(JSON.stringify(response.result, null, 2));
|
|
306
820
|
}
|
|
307
821
|
|
|
308
822
|
async function getToolSchema(toolName: string): Promise<void> {
|
|
309
823
|
const response = await mcpRequest("tools/list");
|
|
310
|
-
|
|
311
824
|
if (response.error) {
|
|
312
825
|
console.error("Error:", response.error.message);
|
|
313
826
|
process.exit(1);
|
|
314
827
|
}
|
|
315
|
-
|
|
316
828
|
const result = response.result as {
|
|
317
829
|
tools: Array<{ name: string; description: string; inputSchema: object }>;
|
|
318
830
|
};
|
|
319
|
-
|
|
320
831
|
const tool = result.tools.find((t) => t.name === toolName);
|
|
321
832
|
if (!tool) {
|
|
322
833
|
console.error(`Tool not found: ${toolName}`);
|
|
@@ -325,15 +836,48 @@ async function getToolSchema(toolName: string): Promise<void> {
|
|
|
325
836
|
);
|
|
326
837
|
process.exit(1);
|
|
327
838
|
}
|
|
328
|
-
|
|
329
839
|
console.log(`Tool: ${tool.name}\n`);
|
|
330
|
-
if (tool.description) {
|
|
331
|
-
console.log(`Description: ${tool.description}\n`);
|
|
332
|
-
}
|
|
840
|
+
if (tool.description) console.log(`Description: ${tool.description}\n`);
|
|
333
841
|
console.log("Input Schema:");
|
|
334
842
|
console.log(JSON.stringify(tool.inputSchema, null, 2));
|
|
335
843
|
}
|
|
336
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
|
+
|
|
337
881
|
function printUsage(): void {
|
|
338
882
|
console.log(`MCP HTTP Client - Connect to any MCP server over HTTP
|
|
339
883
|
|
|
@@ -344,79 +888,86 @@ Commands:
|
|
|
344
888
|
list-resources List available resources
|
|
345
889
|
info <tool> Show tool schema/parameters
|
|
346
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
|
|
347
893
|
|
|
348
894
|
Options:
|
|
349
|
-
--header, -H "K: V" Add HTTP header (repeatable)
|
|
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)
|
|
350
898
|
--help, -h Show this help
|
|
351
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
|
+
|
|
352
906
|
Examples:
|
|
353
|
-
# List tools from a server
|
|
354
907
|
npx tsx mcp-http.ts http://localhost:3001/mcp list-tools
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
npx tsx mcp-http.ts
|
|
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"}'
|
|
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}'
|
|
364
911
|
`);
|
|
365
912
|
}
|
|
366
913
|
|
|
367
914
|
async function main(): Promise<void> {
|
|
368
|
-
const
|
|
915
|
+
const parsed = parseArgs();
|
|
369
916
|
|
|
370
|
-
if (!url) {
|
|
917
|
+
if (!parsed.url) {
|
|
371
918
|
console.error("Error: URL is required\n");
|
|
372
919
|
printUsage();
|
|
373
920
|
process.exit(1);
|
|
374
921
|
}
|
|
375
922
|
|
|
376
|
-
|
|
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) {
|
|
377
930
|
console.error("Error: Command is required\n");
|
|
378
931
|
printUsage();
|
|
379
932
|
process.exit(1);
|
|
380
933
|
}
|
|
381
934
|
|
|
382
|
-
// Set globals
|
|
383
|
-
serverUrl = url;
|
|
384
|
-
requestHeaders = headers;
|
|
385
|
-
|
|
386
935
|
try {
|
|
387
|
-
switch (command) {
|
|
936
|
+
switch (parsed.command) {
|
|
388
937
|
case "list-tools":
|
|
389
938
|
await listTools();
|
|
390
939
|
break;
|
|
391
|
-
|
|
392
940
|
case "list-resources":
|
|
393
941
|
await listResources();
|
|
394
942
|
break;
|
|
395
|
-
|
|
396
943
|
case "info": {
|
|
397
|
-
const [toolName] = commandArgs;
|
|
944
|
+
const [toolName] = parsed.commandArgs;
|
|
398
945
|
if (!toolName) {
|
|
399
|
-
console.error("Error: Tool name required");
|
|
400
|
-
console.error("Usage: info <tool>");
|
|
946
|
+
console.error("Error: Tool name required\nUsage: info <tool>");
|
|
401
947
|
process.exit(1);
|
|
402
948
|
}
|
|
403
949
|
await getToolSchema(toolName);
|
|
404
950
|
break;
|
|
405
951
|
}
|
|
406
|
-
|
|
407
952
|
case "call": {
|
|
408
|
-
const [toolName, argsJson] = commandArgs;
|
|
953
|
+
const [toolName, argsJson] = parsed.commandArgs;
|
|
409
954
|
if (!toolName) {
|
|
410
|
-
console.error(
|
|
411
|
-
|
|
955
|
+
console.error(
|
|
956
|
+
"Error: Tool name required\nUsage: call <tool> '<json-args>'",
|
|
957
|
+
);
|
|
412
958
|
process.exit(1);
|
|
413
959
|
}
|
|
414
960
|
await callTool(toolName, argsJson || "{}");
|
|
415
961
|
break;
|
|
416
962
|
}
|
|
417
|
-
|
|
963
|
+
case "login":
|
|
964
|
+
await loginCommand();
|
|
965
|
+
break;
|
|
966
|
+
case "logout":
|
|
967
|
+
logoutCommand();
|
|
968
|
+
break;
|
|
418
969
|
default:
|
|
419
|
-
console.error(`Unknown command: ${command}\n`);
|
|
970
|
+
console.error(`Unknown command: ${parsed.command}\n`);
|
|
420
971
|
printUsage();
|
|
421
972
|
process.exit(1);
|
|
422
973
|
}
|