@slashfi/agents-sdk 0.76.0 → 0.77.0
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/adk.js +183 -111
- package/dist/adk.js.map +1 -1
- package/dist/cjs/config-store.js +493 -13
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/mcp-client.js +98 -0
- package/dist/cjs/mcp-client.js.map +1 -1
- package/dist/cjs/registry-consumer.js +68 -10
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/config-store.d.ts +36 -1
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +494 -14
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +54 -0
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js.map +1 -1
- package/dist/mcp-client.d.ts +44 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +95 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +68 -10
- package/dist/registry-consumer.js.map +1 -1
- package/package.json +1 -1
- package/src/adk.ts +71 -3
- package/src/config-store.test.ts +212 -0
- package/src/config-store.ts +572 -18
- package/src/define-config.ts +58 -0
- package/src/mcp-client.ts +121 -0
- package/src/registry-consumer.ts +74 -12
package/src/define-config.ts
CHANGED
|
@@ -58,6 +58,49 @@ export interface RegistryProxy {
|
|
|
58
58
|
agent?: string;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* OAuth state captured after `adk registry auth` completes a dynamic-client
|
|
63
|
+
* registration + authorization-code flow against a registry. Stored alongside
|
|
64
|
+
* `auth.token` so the access token can be refreshed without re-prompting the
|
|
65
|
+
* user. The `auth.token` slot holds the current access token; everything
|
|
66
|
+
* needed to refresh it lives here.
|
|
67
|
+
*/
|
|
68
|
+
export interface RegistryOAuthState {
|
|
69
|
+
/** Token endpoint used for code exchange / refresh. */
|
|
70
|
+
tokenEndpoint: string;
|
|
71
|
+
/** Client ID issued by dynamic client registration (RFC 7591). */
|
|
72
|
+
clientId: string;
|
|
73
|
+
/** Client secret from dynamic registration, when the server issued one. */
|
|
74
|
+
clientSecret?: string;
|
|
75
|
+
/** Refresh token returned by the token endpoint, if any. */
|
|
76
|
+
refreshToken?: string;
|
|
77
|
+
/** Absolute expiry (ISO 8601) derived from `expires_in` at exchange time. */
|
|
78
|
+
expiresAt?: string;
|
|
79
|
+
/** Scopes the access token was granted for. */
|
|
80
|
+
scopes?: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Captured auth challenge from a registry that rejected an unauthenticated
|
|
85
|
+
* probe (RFC 6750 `WWW-Authenticate` + RFC 9728 protected-resource metadata).
|
|
86
|
+
* When present on a `RegistryEntry`, the registry has been seen to require
|
|
87
|
+
* credentials and ref ops will fail until `adk registry auth` is run.
|
|
88
|
+
*/
|
|
89
|
+
export interface RegistryAuthRequirement {
|
|
90
|
+
/** Auth scheme advertised in `WWW-Authenticate` (e.g. `Bearer`). */
|
|
91
|
+
scheme?: string;
|
|
92
|
+
/** Realm advertised in `WWW-Authenticate`. */
|
|
93
|
+
realm?: string;
|
|
94
|
+
/** RFC 9728 `resource_metadata` URL parsed from the challenge. */
|
|
95
|
+
resourceMetadataUrl?: string;
|
|
96
|
+
/** Authorization servers from the protected-resource metadata. */
|
|
97
|
+
authorizationServers?: string[];
|
|
98
|
+
/** Scopes supported by the resource. */
|
|
99
|
+
scopes?: string[];
|
|
100
|
+
/** Bearer-methods advertised by the resource (`header`, `body`, `query`). */
|
|
101
|
+
bearerMethodsSupported?: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
61
104
|
/** A registry endpoint the consumer connects to */
|
|
62
105
|
export interface RegistryEntry {
|
|
63
106
|
/** Registry URL (e.g., 'https://registry.slash.com') */
|
|
@@ -84,6 +127,21 @@ export interface RegistryEntry {
|
|
|
84
127
|
* running locally. See {@link RegistryProxy}.
|
|
85
128
|
*/
|
|
86
129
|
proxy?: RegistryProxy;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Populated by `adk registry add` when the probe returned 401. Cleared
|
|
133
|
+
* by `adk registry auth`. Registry ops refuse to run while this is set
|
|
134
|
+
* and no usable auth credentials are configured.
|
|
135
|
+
*/
|
|
136
|
+
authRequirement?: RegistryAuthRequirement;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* OAuth lifecycle state — refresh token, client credentials from dynamic
|
|
140
|
+
* registration, token endpoint, expiry. Populated by `adk registry auth`
|
|
141
|
+
* when the flow went through OAuth; absent for manually-supplied bearer
|
|
142
|
+
* tokens.
|
|
143
|
+
*/
|
|
144
|
+
oauth?: RegistryOAuthState;
|
|
87
145
|
}
|
|
88
146
|
|
|
89
147
|
// ============================================
|
package/src/mcp-client.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { generatePkcePair } from "./pkce.js";
|
|
17
|
+
import type { RegistryAuthRequirement } from "./define-config.js";
|
|
18
|
+
import type { FetchFn } from "./fetch-types.js";
|
|
17
19
|
|
|
18
20
|
// ============================================
|
|
19
21
|
// Types
|
|
@@ -239,3 +241,122 @@ export async function refreshAccessToken(
|
|
|
239
241
|
expiresIn: data.expires_in as number | undefined,
|
|
240
242
|
};
|
|
241
243
|
}
|
|
244
|
+
|
|
245
|
+
// ============================================
|
|
246
|
+
// Registry Auth Probe (RFC 6750 + RFC 9728)
|
|
247
|
+
// ============================================
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Parse a `WWW-Authenticate` header of the form
|
|
251
|
+
* `Bearer realm="x", resource_metadata="https://..."`
|
|
252
|
+
* Returns the scheme and any `key="value"` params. Tolerant of
|
|
253
|
+
* single-value headers and missing params.
|
|
254
|
+
*/
|
|
255
|
+
export function parseWwwAuthenticate(
|
|
256
|
+
header: string,
|
|
257
|
+
): { scheme: string; params: Record<string, string> } {
|
|
258
|
+
const spaceIdx = header.indexOf(" ");
|
|
259
|
+
const scheme = (spaceIdx === -1 ? header : header.slice(0, spaceIdx)).trim();
|
|
260
|
+
const rest = spaceIdx === -1 ? "" : header.slice(spaceIdx + 1);
|
|
261
|
+
const params: Record<string, string> = {};
|
|
262
|
+
for (const match of rest.matchAll(/([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"/g)) {
|
|
263
|
+
params[match[1]!.toLowerCase()] = match[2]!;
|
|
264
|
+
}
|
|
265
|
+
return { scheme, params };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** RFC 9728 protected-resource metadata. */
|
|
269
|
+
export interface ProtectedResourceMetadata {
|
|
270
|
+
resource: string;
|
|
271
|
+
authorization_servers?: string[];
|
|
272
|
+
bearer_methods_supported?: string[];
|
|
273
|
+
scopes_supported?: string[];
|
|
274
|
+
resource_documentation?: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Fetch RFC 9728 metadata. Returns null on any failure. */
|
|
278
|
+
export async function discoverProtectedResourceMetadata(
|
|
279
|
+
metadataUrl: string,
|
|
280
|
+
fetchFn: FetchFn = globalThis.fetch,
|
|
281
|
+
): Promise<ProtectedResourceMetadata | null> {
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetchFn(metadataUrl);
|
|
284
|
+
if (!res.ok) return null;
|
|
285
|
+
return (await res.json()) as ProtectedResourceMetadata;
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Probe an MCP URL to see whether it requires authentication.
|
|
293
|
+
*
|
|
294
|
+
* Sends a minimal `initialize` request. On 200 the server accepts anonymous
|
|
295
|
+
* connections; on 401 we parse the `WWW-Authenticate` header and, if it
|
|
296
|
+
* points at RFC 9728 resource metadata, fetch it so the caller can record
|
|
297
|
+
* the authorization servers and scopes.
|
|
298
|
+
*
|
|
299
|
+
* Returns `{ ok: true }` when no auth is required, or
|
|
300
|
+
* `{ ok: false, requirement }` when the server challenged the request.
|
|
301
|
+
* Other failures (DNS, TLS, unexpected status) surface as `{ ok: null }`
|
|
302
|
+
* — the caller should treat those as probe-inconclusive rather than
|
|
303
|
+
* asserting auth is required.
|
|
304
|
+
*/
|
|
305
|
+
export async function probeRegistryAuth(
|
|
306
|
+
registryUrl: string,
|
|
307
|
+
fetchFn: FetchFn = globalThis.fetch,
|
|
308
|
+
): Promise<
|
|
309
|
+
| { ok: true }
|
|
310
|
+
| { ok: false; requirement: RegistryAuthRequirement }
|
|
311
|
+
| { ok: null }
|
|
312
|
+
> {
|
|
313
|
+
const url = registryUrl.replace(/\/$/, "");
|
|
314
|
+
let res: Response;
|
|
315
|
+
try {
|
|
316
|
+
res = await fetchFn(url, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
319
|
+
body: JSON.stringify({
|
|
320
|
+
jsonrpc: "2.0",
|
|
321
|
+
id: 1,
|
|
322
|
+
method: "initialize",
|
|
323
|
+
params: {
|
|
324
|
+
protocolVersion: "2024-11-05",
|
|
325
|
+
capabilities: {},
|
|
326
|
+
clientInfo: { name: "agents-sdk-probe", version: "1.0.0" },
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
});
|
|
330
|
+
} catch {
|
|
331
|
+
return { ok: null };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (res.status !== 401) {
|
|
335
|
+
return res.ok ? { ok: true } : { ok: null };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const wwwAuth = res.headers.get("www-authenticate") ?? "";
|
|
339
|
+
const { scheme, params } = parseWwwAuthenticate(wwwAuth);
|
|
340
|
+
const requirement: RegistryAuthRequirement = {};
|
|
341
|
+
if (scheme) requirement.scheme = scheme;
|
|
342
|
+
if (params.realm) requirement.realm = params.realm;
|
|
343
|
+
|
|
344
|
+
const metadataUrl = params.resource_metadata;
|
|
345
|
+
if (metadataUrl) {
|
|
346
|
+
requirement.resourceMetadataUrl = metadataUrl;
|
|
347
|
+
const metadata = await discoverProtectedResourceMetadata(metadataUrl, fetchFn);
|
|
348
|
+
if (metadata) {
|
|
349
|
+
if (metadata.authorization_servers?.length) {
|
|
350
|
+
requirement.authorizationServers = metadata.authorization_servers;
|
|
351
|
+
}
|
|
352
|
+
if (metadata.scopes_supported?.length) {
|
|
353
|
+
requirement.scopes = metadata.scopes_supported;
|
|
354
|
+
}
|
|
355
|
+
if (metadata.bearer_methods_supported?.length) {
|
|
356
|
+
requirement.bearerMethodsSupported = metadata.bearer_methods_supported;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return { ok: false, requirement };
|
|
362
|
+
}
|
package/src/registry-consumer.ts
CHANGED
|
@@ -41,6 +41,8 @@ import type {
|
|
|
41
41
|
import type { CallAgentRequest } from "./call-agent-schema.js";
|
|
42
42
|
import type { FetchFn } from "./fetch-types.js";
|
|
43
43
|
import type { SecuritySchemeSummary, CallAgentResponse } from "./types.js";
|
|
44
|
+
import { AdkError } from "./adk-error.js";
|
|
45
|
+
import { parseWwwAuthenticate } from "./mcp-client.js";
|
|
44
46
|
import {
|
|
45
47
|
isSecretUri,
|
|
46
48
|
normalizeRef,
|
|
@@ -391,12 +393,19 @@ async function listFromMcpServer(
|
|
|
391
393
|
...(params && { params }),
|
|
392
394
|
}),
|
|
393
395
|
});
|
|
396
|
+
if (res.status === 401) throwRegistryAuthError(serverUrl, res);
|
|
394
397
|
if (!res.ok) {
|
|
395
|
-
|
|
398
|
+
const body = await res.text().catch(() => "");
|
|
399
|
+
throwRegistryCallError(serverUrl, res.status, body);
|
|
396
400
|
}
|
|
397
401
|
const json = (await res.json()) as { result?: unknown; error?: { message: string } };
|
|
398
402
|
if (json.error) {
|
|
399
|
-
throw new
|
|
403
|
+
throw new AdkError({
|
|
404
|
+
code: "registry_rpc_error",
|
|
405
|
+
message: `Registry at ${serverUrl} returned an RPC error: ${json.error.message}`,
|
|
406
|
+
hint: "Check the tool name and parameters, or inspect the registry.",
|
|
407
|
+
details: { url: serverUrl, rpcError: json.error.message },
|
|
408
|
+
});
|
|
400
409
|
}
|
|
401
410
|
return json.result;
|
|
402
411
|
}
|
|
@@ -466,12 +475,19 @@ async function discoverRegistryViaMcp(
|
|
|
466
475
|
...(params && { params }),
|
|
467
476
|
}),
|
|
468
477
|
});
|
|
478
|
+
if (res.status === 401) throwRegistryAuthError(serverUrl, res);
|
|
469
479
|
if (!res.ok) {
|
|
470
|
-
|
|
480
|
+
const body = await res.text().catch(() => "");
|
|
481
|
+
throwRegistryCallError(serverUrl, res.status, body);
|
|
471
482
|
}
|
|
472
483
|
const json = (await res.json()) as { result?: unknown; error?: { message: string } };
|
|
473
484
|
if (json.error) {
|
|
474
|
-
throw new
|
|
485
|
+
throw new AdkError({
|
|
486
|
+
code: "registry_rpc_error",
|
|
487
|
+
message: `Registry at ${serverUrl} returned an RPC error: ${json.error.message}`,
|
|
488
|
+
hint: "Check the registry URL and that the server speaks MCP.",
|
|
489
|
+
details: { url: serverUrl, rpcError: json.error.message },
|
|
490
|
+
});
|
|
475
491
|
}
|
|
476
492
|
return json.result;
|
|
477
493
|
}
|
|
@@ -513,6 +529,45 @@ async function discoverRegistryViaMcp(
|
|
|
513
529
|
};
|
|
514
530
|
}
|
|
515
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Throw a typed auth error with context parsed from the challenge. Used when
|
|
534
|
+
* any MCP call returns 401, so callers see a friendly message pointing at
|
|
535
|
+
* `adk registry auth` rather than a bare "MCP call failed: 401".
|
|
536
|
+
*/
|
|
537
|
+
function throwRegistryAuthError(
|
|
538
|
+
serverUrl: string,
|
|
539
|
+
res: Response,
|
|
540
|
+
): never {
|
|
541
|
+
const wwwAuth = res.headers.get("www-authenticate") ?? "";
|
|
542
|
+
const { scheme, params } = parseWwwAuthenticate(wwwAuth);
|
|
543
|
+
throw new AdkError({
|
|
544
|
+
code: "registry_auth_required",
|
|
545
|
+
message: `Registry at ${serverUrl} returned 401 Unauthorized.`,
|
|
546
|
+
hint: `Run: adk registry auth <name> --token <token>`,
|
|
547
|
+
details: {
|
|
548
|
+
url: serverUrl,
|
|
549
|
+
scheme,
|
|
550
|
+
realm: params.realm,
|
|
551
|
+
resourceMetadataUrl: params.resource_metadata,
|
|
552
|
+
status: 401,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Wrap a generic MCP failure (non-401) in an AdkError so CLI callers get a typed error. */
|
|
558
|
+
function throwRegistryCallError(
|
|
559
|
+
serverUrl: string,
|
|
560
|
+
status: number,
|
|
561
|
+
body: string,
|
|
562
|
+
): never {
|
|
563
|
+
throw new AdkError({
|
|
564
|
+
code: "registry_call_failed",
|
|
565
|
+
message: `Registry at ${serverUrl} returned ${status}.`,
|
|
566
|
+
hint: "Check the registry URL and that the server is reachable.",
|
|
567
|
+
details: { url: serverUrl, status, body: body.slice(0, 500) },
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
516
571
|
/**
|
|
517
572
|
* Call a tool on a direct MCP server.
|
|
518
573
|
*/
|
|
@@ -542,12 +597,19 @@ async function callMcpTool(
|
|
|
542
597
|
params: { name: toolName, arguments: params },
|
|
543
598
|
}),
|
|
544
599
|
});
|
|
600
|
+
if (res.status === 401) throwRegistryAuthError(serverUrl, res);
|
|
545
601
|
if (!res.ok) {
|
|
546
|
-
|
|
602
|
+
const body = await res.text().catch(() => "");
|
|
603
|
+
throwRegistryCallError(serverUrl, res.status, body);
|
|
547
604
|
}
|
|
548
605
|
const json = (await res.json()) as { result?: unknown; error?: { message: string } };
|
|
549
606
|
if (json.error) {
|
|
550
|
-
throw new
|
|
607
|
+
throw new AdkError({
|
|
608
|
+
code: "registry_rpc_error",
|
|
609
|
+
message: `Registry at ${serverUrl} returned an RPC error: ${json.error.message}`,
|
|
610
|
+
hint: "Check the tool name and parameters, or inspect the registry.",
|
|
611
|
+
details: { url: serverUrl, rpcError: json.error.message },
|
|
612
|
+
});
|
|
551
613
|
}
|
|
552
614
|
|
|
553
615
|
// Extract text content
|
|
@@ -977,19 +1039,19 @@ export async function createRegistryConsumer(
|
|
|
977
1039
|
},
|
|
978
1040
|
|
|
979
1041
|
async browse(registryUrl?: string, query?: string): Promise<AgentListEntry[]> {
|
|
980
|
-
// List agents from a specific registry, or all registries if not specified
|
|
1042
|
+
// List agents from a specific registry, or all registries if not specified.
|
|
1043
|
+
// Errors from any target propagate — previously allSettled silently
|
|
1044
|
+
// dropped rejections (including 401s), so browsing an unreachable or
|
|
1045
|
+
// unauthenticated registry returned 0 agents with no indication why.
|
|
981
1046
|
const targets = registryUrl
|
|
982
1047
|
? resolvedRegistries.filter(
|
|
983
1048
|
(r) => r.url === registryUrl || r.name === registryUrl,
|
|
984
1049
|
)
|
|
985
1050
|
: resolvedRegistries;
|
|
986
|
-
|
|
987
|
-
const results = await Promise.allSettled(
|
|
1051
|
+
const results = await Promise.all(
|
|
988
1052
|
targets.map((t) => listFromRegistry(t, query)),
|
|
989
1053
|
);
|
|
990
|
-
return results.
|
|
991
|
-
r.status === "fulfilled" ? r.value : [],
|
|
992
|
-
);
|
|
1054
|
+
return results.flat();
|
|
993
1055
|
},
|
|
994
1056
|
|
|
995
1057
|
async inspect(
|