@slashfi/agents-sdk 0.90.2 → 0.90.5
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/cjs/config-store.js +294 -54
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/mcp-client.js +77 -24
- package/dist/cjs/mcp-client.js.map +1 -1
- package/dist/config-store.d.ts +7 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +294 -54
- package/dist/config-store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-client.d.ts +5 -2
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +77 -24
- package/dist/mcp-client.js.map +1 -1
- package/package.json +1 -1
- package/src/config-store.test.ts +385 -0
- package/src/config-store.ts +408 -74
- package/src/index.ts +4 -1
- package/src/mcp-client.test.ts +80 -0
- package/src/mcp-client.ts +114 -31
package/src/config-store.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { RegistryAuthRequirement } from "./define-config.js";
|
|
|
33
33
|
import type { FetchFn } from "./fetch-types.js";
|
|
34
34
|
import type { Logger } from "./logger.js";
|
|
35
35
|
import {
|
|
36
|
+
type OAuthClientAuthMethod,
|
|
36
37
|
buildOAuthAuthorizeUrl,
|
|
37
38
|
discoverOAuthMetadata,
|
|
38
39
|
dynamicClientRegistration,
|
|
@@ -111,6 +112,11 @@ export interface RegistryCacheAuthField {
|
|
|
111
112
|
* For example HTTP Basic stores one `token` but asks UI for username/password.
|
|
112
113
|
*/
|
|
113
114
|
parts?: RegistryCacheAuthFieldPart[];
|
|
115
|
+
/**
|
|
116
|
+
* When `false`, connect/refresh bookkeeping only — not forwarded on
|
|
117
|
+
* `ref.call`. Omitted or `true` for bearer, header, and call-time creds.
|
|
118
|
+
*/
|
|
119
|
+
outbound?: boolean;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
/**
|
|
@@ -391,6 +397,8 @@ export interface CredentialField {
|
|
|
391
397
|
format?: CompositeCredentialFormat;
|
|
392
398
|
/** Structured inputs that compose this canonical stored credential */
|
|
393
399
|
parts?: AuthChallengeField[];
|
|
400
|
+
/** Connect/refresh only — not forwarded on ref.call. */
|
|
401
|
+
outbound?: boolean;
|
|
394
402
|
}
|
|
395
403
|
|
|
396
404
|
/** Describes what auth a ref needs and what's already provided */
|
|
@@ -991,6 +999,205 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
991
999
|
};
|
|
992
1000
|
}
|
|
993
1001
|
|
|
1002
|
+
/**
|
|
1003
|
+
* Call-time credential lookup: stored ref config first, then the host
|
|
1004
|
+
* `resolveCredentials` callback. Does not persist resolved values.
|
|
1005
|
+
*/
|
|
1006
|
+
async function resolveCallCredential(
|
|
1007
|
+
ctx: CredentialResolverContext,
|
|
1008
|
+
field: string,
|
|
1009
|
+
): Promise<string | null> {
|
|
1010
|
+
const stored = await readRefSecret(ctx.name, field);
|
|
1011
|
+
if (stored) return stored;
|
|
1012
|
+
return makeTryResolve(ctx)(field);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const CALL_BEARER_FIELDS = ["access_token", "api_key", "token"] as const;
|
|
1016
|
+
|
|
1017
|
+
function isBearerAuthField(field: string): boolean {
|
|
1018
|
+
return (CALL_BEARER_FIELDS as readonly string[]).includes(field);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** Legacy cache entries may omit `outbound`; these are never call-time creds. */
|
|
1022
|
+
const LEGACY_CONNECT_ONLY_FIELDS = new Set([
|
|
1023
|
+
"client_id",
|
|
1024
|
+
"client_secret",
|
|
1025
|
+
"refresh_token",
|
|
1026
|
+
]);
|
|
1027
|
+
|
|
1028
|
+
function isCallOutboundAuthField(
|
|
1029
|
+
field: string,
|
|
1030
|
+
info: RegistryCacheAuthField,
|
|
1031
|
+
): boolean {
|
|
1032
|
+
if (info.outbound === false) return false;
|
|
1033
|
+
if (info.outbound === true) return true;
|
|
1034
|
+
return !LEGACY_CONNECT_ONLY_FIELDS.has(field);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function readRegistryDeclaredAuthFields(
|
|
1038
|
+
security: SecuritySchemeSummary | null,
|
|
1039
|
+
): Record<string, RegistryCacheAuthField> | undefined {
|
|
1040
|
+
if (!security || typeof security !== "object") return undefined;
|
|
1041
|
+
const raw = (security as { authFields?: unknown }).authFields;
|
|
1042
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
1043
|
+
const out: Record<string, RegistryCacheAuthField> = {};
|
|
1044
|
+
for (const [field, meta] of Object.entries(
|
|
1045
|
+
raw as Record<string, unknown>,
|
|
1046
|
+
)) {
|
|
1047
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) continue;
|
|
1048
|
+
const m = meta as Record<string, unknown>;
|
|
1049
|
+
if (typeof m.required !== "boolean" || typeof m.automated !== "boolean") {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
out[field] = { required: m.required, automated: m.automated };
|
|
1053
|
+
if (typeof m.outbound === "boolean") {
|
|
1054
|
+
out[field].outbound = m.outbound;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async function mergeRegistryDeclaredAuthFields(
|
|
1061
|
+
fields: Record<string, CredentialField>,
|
|
1062
|
+
declared: Record<string, RegistryCacheAuthField> | undefined,
|
|
1063
|
+
canResolve: (field: string) => Promise<boolean>,
|
|
1064
|
+
configKeys: string[],
|
|
1065
|
+
refConfig: Record<string, unknown>,
|
|
1066
|
+
): Promise<Record<string, CredentialField>> {
|
|
1067
|
+
if (!declared) return fields;
|
|
1068
|
+
const next: Record<string, CredentialField> = {};
|
|
1069
|
+
for (const [field, meta] of Object.entries(declared)) {
|
|
1070
|
+
next[field] = {
|
|
1071
|
+
required: meta.required,
|
|
1072
|
+
automated: meta.automated,
|
|
1073
|
+
present:
|
|
1074
|
+
configKeys.includes(field) || hasCredentialField(refConfig, field),
|
|
1075
|
+
resolvable: await canResolve(field),
|
|
1076
|
+
...(meta.format && { format: meta.format }),
|
|
1077
|
+
...(meta.parts && { parts: meta.parts }),
|
|
1078
|
+
...(meta.outbound === false && { outbound: false }),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return next;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function bearerFieldSatisfied(
|
|
1085
|
+
accessToken: string | null,
|
|
1086
|
+
refConfig: Record<string, unknown>,
|
|
1087
|
+
field: string,
|
|
1088
|
+
): boolean {
|
|
1089
|
+
if (accessToken) return true;
|
|
1090
|
+
return hasCredentialField(refConfig, field);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function fallbackCallAuthFields(): Record<string, RegistryCacheAuthField> {
|
|
1094
|
+
return {
|
|
1095
|
+
access_token: { required: true, automated: true },
|
|
1096
|
+
api_key: { required: false, automated: true },
|
|
1097
|
+
token: { required: false, automated: true },
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function headerFieldSatisfied(
|
|
1102
|
+
headers: Record<string, string>,
|
|
1103
|
+
field: string,
|
|
1104
|
+
): boolean {
|
|
1105
|
+
const wanted = normalizeCredentialKey(field);
|
|
1106
|
+
return Object.keys(headers).some(
|
|
1107
|
+
(key) => normalizeCredentialKey(key) === wanted,
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function resolveHeaderNameForField(
|
|
1112
|
+
field: string,
|
|
1113
|
+
refConfig: Record<string, unknown>,
|
|
1114
|
+
): string {
|
|
1115
|
+
const wanted = normalizeCredentialKey(field);
|
|
1116
|
+
const configHeaders = refConfig.headers;
|
|
1117
|
+
if (
|
|
1118
|
+
configHeaders &&
|
|
1119
|
+
typeof configHeaders === "object" &&
|
|
1120
|
+
!Array.isArray(configHeaders)
|
|
1121
|
+
) {
|
|
1122
|
+
for (const key of Object.keys(configHeaders as Record<string, unknown>)) {
|
|
1123
|
+
if (normalizeCredentialKey(key) === wanted) return key;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
// x_api_key → X-API-KEY (registry codegen declares the canonical name;
|
|
1127
|
+
// env-resolved keys use the normalized storage field name).
|
|
1128
|
+
return field
|
|
1129
|
+
.split("_")
|
|
1130
|
+
.filter(Boolean)
|
|
1131
|
+
.map((part) => part.toUpperCase())
|
|
1132
|
+
.join("-");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Supplement call-time credentials from `resolveCredentials` when they
|
|
1137
|
+
* are not already present in consumer-config. Stored values and config
|
|
1138
|
+
* headers win — this only fills gaps. Walks cached `authFields` as the
|
|
1139
|
+
* source of truth (registry-declared when auth-status has run).
|
|
1140
|
+
*/
|
|
1141
|
+
async function resolveAllCallCredentials(opts: {
|
|
1142
|
+
ctx: CredentialResolverContext;
|
|
1143
|
+
refConfig: Record<string, unknown>;
|
|
1144
|
+
accessToken: string | null;
|
|
1145
|
+
resolvedHeaders: Record<string, string> | undefined;
|
|
1146
|
+
}): Promise<{
|
|
1147
|
+
accessToken: string | null;
|
|
1148
|
+
resolvedHeaders: Record<string, string> | undefined;
|
|
1149
|
+
}> {
|
|
1150
|
+
if (!options.resolveCredentials) {
|
|
1151
|
+
return {
|
|
1152
|
+
accessToken: opts.accessToken,
|
|
1153
|
+
resolvedHeaders: opts.resolvedHeaders,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
let accessToken = opts.accessToken;
|
|
1158
|
+
let resolvedHeaders = opts.resolvedHeaders;
|
|
1159
|
+
const { ctx, refConfig } = opts;
|
|
1160
|
+
|
|
1161
|
+
const cache = await readRegistryCache();
|
|
1162
|
+
const authFields =
|
|
1163
|
+
cache.refs[ctx.name]?.authFields ?? fallbackCallAuthFields();
|
|
1164
|
+
|
|
1165
|
+
for (const [field, info] of Object.entries(authFields)) {
|
|
1166
|
+
if (!isCallOutboundAuthField(field, info)) continue;
|
|
1167
|
+
if (!info.required && !info.automated) continue;
|
|
1168
|
+
|
|
1169
|
+
if (isBearerAuthField(field)) {
|
|
1170
|
+
if (bearerFieldSatisfied(accessToken, refConfig, field)) continue;
|
|
1171
|
+
const value = await resolveCallCredential(ctx, field);
|
|
1172
|
+
if (value) accessToken = accessToken ?? value;
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (hasCredentialField(refConfig, field)) continue;
|
|
1177
|
+
if (resolvedHeaders && headerFieldSatisfied(resolvedHeaders, field)) {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const value = await resolveCallCredential(ctx, field);
|
|
1182
|
+
if (!value) continue;
|
|
1183
|
+
|
|
1184
|
+
resolvedHeaders = resolvedHeaders ?? {};
|
|
1185
|
+
if (!headerFieldSatisfied(resolvedHeaders, field)) {
|
|
1186
|
+
resolvedHeaders[resolveHeaderNameForField(field, refConfig)] = value;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (!accessToken) {
|
|
1191
|
+
const username = await resolveCallCredential(ctx, "username");
|
|
1192
|
+
const password = await resolveCallCredential(ctx, "password");
|
|
1193
|
+
if (username && password) {
|
|
1194
|
+
accessToken = btoa(`${username}:${password}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
return { accessToken, resolvedHeaders };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
994
1201
|
/**
|
|
995
1202
|
* Resolve OAuth client credentials (client_id + client_secret) for a
|
|
996
1203
|
* ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
|
|
@@ -1023,6 +1230,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1023
1230
|
codeVerifier: string;
|
|
1024
1231
|
clientId: string;
|
|
1025
1232
|
clientSecret?: string;
|
|
1233
|
+
clientAuthMethod?: OAuthClientAuthMethod;
|
|
1026
1234
|
tokenEndpoint: string;
|
|
1027
1235
|
redirectUri: string;
|
|
1028
1236
|
createdAt: number;
|
|
@@ -1230,6 +1438,109 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1230
1438
|
}
|
|
1231
1439
|
}
|
|
1232
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Resolve OAuth server metadata from a registry security scheme.
|
|
1443
|
+
* Shared by `ref.auth` and `ref.refreshToken` so both paths discover
|
|
1444
|
+
* token endpoints the same way — explicit manifest URLs, RFC 8414
|
|
1445
|
+
* discovery via `discoveryUrl`, authorization-server discovery, and
|
|
1446
|
+
* finally the MCP upstream URL for redirect-mode agents.
|
|
1447
|
+
*/
|
|
1448
|
+
async function resolveOAuthMetadataFromSecurity(
|
|
1449
|
+
security: SecuritySchemeSummary | null | undefined,
|
|
1450
|
+
opts?: { serverUrl?: string },
|
|
1451
|
+
): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
|
|
1452
|
+
if (!security || security.type !== "oauth2") return null;
|
|
1453
|
+
|
|
1454
|
+
const securityExt = security as {
|
|
1455
|
+
discoveryUrl?: string;
|
|
1456
|
+
flows?: {
|
|
1457
|
+
authorizationCode?: {
|
|
1458
|
+
authorizationUrl?: string;
|
|
1459
|
+
tokenUrl?: string;
|
|
1460
|
+
refreshUrl?: string;
|
|
1461
|
+
scopes?: Record<string, string>;
|
|
1462
|
+
};
|
|
1463
|
+
};
|
|
1464
|
+
};
|
|
1465
|
+
const authCodeFlow = securityExt.flows?.authorizationCode;
|
|
1466
|
+
|
|
1467
|
+
const explicitEndpoint = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
|
|
1468
|
+
if (explicitEndpoint) {
|
|
1469
|
+
const flowScopes = (authCodeFlow as Record<string, unknown> | undefined)
|
|
1470
|
+
?.scopes as Record<string, string> | undefined;
|
|
1471
|
+
const authUrl = authCodeFlow?.authorizationUrl;
|
|
1472
|
+
return {
|
|
1473
|
+
issuer: authUrl
|
|
1474
|
+
? new URL(authUrl).origin
|
|
1475
|
+
: new URL(explicitEndpoint).origin,
|
|
1476
|
+
authorization_endpoint: authUrl ?? explicitEndpoint,
|
|
1477
|
+
token_endpoint: explicitEndpoint,
|
|
1478
|
+
scopes_supported: flowScopes ? Object.keys(flowScopes) : undefined,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (securityExt.discoveryUrl) {
|
|
1483
|
+
const fromDiscovery =
|
|
1484
|
+
(await tryFetchOAuthMetadata(securityExt.discoveryUrl)) ??
|
|
1485
|
+
(await discoverOAuthMetadata(securityExt.discoveryUrl));
|
|
1486
|
+
if (fromDiscovery) return fromDiscovery;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const authUrl = authCodeFlow?.authorizationUrl;
|
|
1490
|
+
if (authUrl) {
|
|
1491
|
+
let metadata = await tryFetchOAuthMetadata(authUrl);
|
|
1492
|
+
if (!metadata) {
|
|
1493
|
+
metadata = await discoverOAuthMetadata(new URL(authUrl).origin);
|
|
1494
|
+
}
|
|
1495
|
+
if (metadata) return metadata;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const serverUrl = opts?.serverUrl;
|
|
1499
|
+
if (serverUrl) {
|
|
1500
|
+
let metadata = await discoverOAuthMetadata(serverUrl);
|
|
1501
|
+
if (!metadata) {
|
|
1502
|
+
metadata = await discoverOAuthMetadata(
|
|
1503
|
+
serverUrl.replace(/\/(mcp|sse)$/, ""),
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
if (metadata) return metadata;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function resolveClientAuthMethod(
|
|
1513
|
+
security: SecuritySchemeSummary | null | undefined,
|
|
1514
|
+
metadata: OAuthServerMetadata | null,
|
|
1515
|
+
): OAuthClientAuthMethod {
|
|
1516
|
+
const flowAuth = (
|
|
1517
|
+
security as {
|
|
1518
|
+
flows?: {
|
|
1519
|
+
authorizationCode?: { clientAuth?: OAuthClientAuthMethod };
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
).flows?.authorizationCode?.clientAuth;
|
|
1523
|
+
if (flowAuth) return flowAuth;
|
|
1524
|
+
|
|
1525
|
+
const supported = metadata?.token_endpoint_auth_methods_supported;
|
|
1526
|
+
if (supported?.length === 1 && supported[0] === "client_secret_basic") {
|
|
1527
|
+
return "client_secret_basic";
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const tokenEndpoint = metadata?.token_endpoint;
|
|
1531
|
+
if (tokenEndpoint) {
|
|
1532
|
+
try {
|
|
1533
|
+
if (new URL(tokenEndpoint).hostname === "api.x.com") {
|
|
1534
|
+
return "client_secret_basic";
|
|
1535
|
+
}
|
|
1536
|
+
} catch {
|
|
1537
|
+
/* ignore malformed token endpoint */
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
return "client_secret_post";
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1233
1544
|
/**
|
|
1234
1545
|
* Build a registryConsumer from the current config.
|
|
1235
1546
|
* Decrypts secret: values in registry headers/auth before connecting.
|
|
@@ -1364,7 +1675,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1364
1675
|
});
|
|
1365
1676
|
if (!found) return false;
|
|
1366
1677
|
for (const r of registries) {
|
|
1367
|
-
if (
|
|
1678
|
+
if (
|
|
1679
|
+
typeof r !== "string" &&
|
|
1680
|
+
(registryDisplayName(r) === nameOrUrl || registryUrl(r) === nameOrUrl)
|
|
1681
|
+
) {
|
|
1368
1682
|
await mutate(r);
|
|
1369
1683
|
}
|
|
1370
1684
|
}
|
|
@@ -2254,7 +2568,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2254
2568
|
const entry = findRef(config.refs ?? [], name);
|
|
2255
2569
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
2256
2570
|
|
|
2257
|
-
|
|
2571
|
+
let accessToken =
|
|
2258
2572
|
(await readRefSecret(name, "access_token")) ??
|
|
2259
2573
|
(await readRefSecret(name, "api_key")) ??
|
|
2260
2574
|
(await readRefSecret(name, "token"));
|
|
@@ -2306,6 +2620,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2306
2620
|
}
|
|
2307
2621
|
}
|
|
2308
2622
|
|
|
2623
|
+
if (options.resolveCredentials) {
|
|
2624
|
+
const supplemented = await resolveAllCallCredentials({
|
|
2625
|
+
ctx: { name, entry, security: null },
|
|
2626
|
+
refConfig,
|
|
2627
|
+
accessToken,
|
|
2628
|
+
resolvedHeaders,
|
|
2629
|
+
});
|
|
2630
|
+
accessToken = supplemented.accessToken;
|
|
2631
|
+
resolvedHeaders = supplemented.resolvedHeaders;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2309
2634
|
const doCall = async (token: string | null) => {
|
|
2310
2635
|
// Direct MCP only for redirect/proxy agents with an MCP upstream.
|
|
2311
2636
|
// API-mode agents must go through the registry (it does REST translation).
|
|
@@ -2460,7 +2785,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2460
2785
|
return (await tryResolveField(field, oauthMetadata)) !== null;
|
|
2461
2786
|
}
|
|
2462
2787
|
|
|
2463
|
-
|
|
2788
|
+
let fields: Record<string, CredentialField> = {};
|
|
2464
2789
|
|
|
2465
2790
|
if (security.type === "oauth2") {
|
|
2466
2791
|
const securityExt = security as {
|
|
@@ -2506,6 +2831,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2506
2831
|
automated: hasRegistration,
|
|
2507
2832
|
present: configKeys.includes("client_id"),
|
|
2508
2833
|
resolvable: await canResolve("client_id", oauthMetadata),
|
|
2834
|
+
outbound: false,
|
|
2509
2835
|
};
|
|
2510
2836
|
if (needsSecret) {
|
|
2511
2837
|
fields.client_secret = {
|
|
@@ -2513,13 +2839,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2513
2839
|
automated: hasRegistration,
|
|
2514
2840
|
present: configKeys.includes("client_secret"),
|
|
2515
2841
|
resolvable: await canResolve("client_secret", oauthMetadata),
|
|
2842
|
+
outbound: false,
|
|
2516
2843
|
};
|
|
2517
2844
|
}
|
|
2518
2845
|
fields.access_token = {
|
|
2519
2846
|
required: true,
|
|
2520
2847
|
automated: accessTokenAutomated,
|
|
2521
2848
|
present: configKeys.includes("access_token"),
|
|
2522
|
-
resolvable:
|
|
2849
|
+
resolvable: await canResolve("access_token"),
|
|
2523
2850
|
};
|
|
2524
2851
|
} else if (security.type === "apiKey") {
|
|
2525
2852
|
const apiKeySec = security as {
|
|
@@ -2586,7 +2913,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2586
2913
|
format: "basic" as const,
|
|
2587
2914
|
parts: [
|
|
2588
2915
|
{ name: "username", label: "Username", secret: false },
|
|
2589
|
-
{
|
|
2916
|
+
{
|
|
2917
|
+
name: "password",
|
|
2918
|
+
label: "Password",
|
|
2919
|
+
secret: true,
|
|
2920
|
+
optional: true,
|
|
2921
|
+
},
|
|
2590
2922
|
],
|
|
2591
2923
|
}),
|
|
2592
2924
|
};
|
|
@@ -2607,8 +2939,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2607
2939
|
};
|
|
2608
2940
|
}
|
|
2609
2941
|
|
|
2942
|
+
fields = await mergeRegistryDeclaredAuthFields(
|
|
2943
|
+
fields,
|
|
2944
|
+
readRegistryDeclaredAuthFields(security),
|
|
2945
|
+
canResolve,
|
|
2946
|
+
configKeys,
|
|
2947
|
+
(entry.config ?? {}) as Record<string, unknown>,
|
|
2948
|
+
);
|
|
2949
|
+
|
|
2610
2950
|
const complete = Object.values(fields).every(
|
|
2611
|
-
(f) => !f.required || f.present || f.resolvable,
|
|
2951
|
+
(f) => !f.required || f.automated || f.present || f.resolvable,
|
|
2612
2952
|
);
|
|
2613
2953
|
|
|
2614
2954
|
// Persist the slim {required, automated} per-field shape into the
|
|
@@ -2624,6 +2964,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2624
2964
|
automated: info.automated,
|
|
2625
2965
|
...(info.format && { format: info.format }),
|
|
2626
2966
|
...(info.parts && { parts: info.parts }),
|
|
2967
|
+
...(info.outbound === false && { outbound: false }),
|
|
2627
2968
|
};
|
|
2628
2969
|
}
|
|
2629
2970
|
await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
|
|
@@ -2761,8 +3102,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2761
3102
|
const username =
|
|
2762
3103
|
opts?.credentials?.["username"] ?? (await tryResolve("username"));
|
|
2763
3104
|
const password =
|
|
2764
|
-
opts?.credentials?.["password"] ??
|
|
2765
|
-
|
|
3105
|
+
opts?.credentials?.["password"] ??
|
|
3106
|
+
(await tryResolve("password")) ??
|
|
3107
|
+
"";
|
|
3108
|
+
const hasUsername =
|
|
3109
|
+
username !== undefined && username !== null && username !== "";
|
|
2766
3110
|
if (!hasUsername) {
|
|
2767
3111
|
return {
|
|
2768
3112
|
type: "http",
|
|
@@ -2823,23 +3167,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2823
3167
|
}
|
|
2824
3168
|
|
|
2825
3169
|
const authUrl = authCodeFlow.authorizationUrl;
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
metadata = await discoverOAuthMetadata(origin);
|
|
2830
|
-
}
|
|
2831
|
-
// Fallback: construct metadata from the security scheme's explicit URLs
|
|
2832
|
-
if (!metadata && authCodeFlow.tokenUrl) {
|
|
2833
|
-
const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
|
|
2834
|
-
| Record<string, string>
|
|
2835
|
-
| undefined;
|
|
2836
|
-
metadata = {
|
|
2837
|
-
issuer: new URL(authUrl).origin,
|
|
2838
|
-
authorization_endpoint: authUrl,
|
|
2839
|
-
token_endpoint: authCodeFlow.tokenUrl,
|
|
2840
|
-
scopes_supported: flowScopes ? Object.keys(flowScopes) : undefined,
|
|
2841
|
-
} as import("./mcp-client.js").OAuthServerMetadata;
|
|
2842
|
-
}
|
|
3170
|
+
const metadata = await resolveOAuthMetadataFromSecurity(security, {
|
|
3171
|
+
serverUrl: entry.url,
|
|
3172
|
+
});
|
|
2843
3173
|
if (!metadata) {
|
|
2844
3174
|
throw new Error(`Could not discover OAuth metadata from ${authUrl}`);
|
|
2845
3175
|
}
|
|
@@ -2950,11 +3280,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2950
3280
|
});
|
|
2951
3281
|
|
|
2952
3282
|
// Persist pending state so handleCallback works across processes
|
|
3283
|
+
const clientAuthMethod = resolveClientAuthMethod(security, metadata);
|
|
2953
3284
|
await storePendingOAuth(state, {
|
|
2954
3285
|
refName: name,
|
|
2955
3286
|
codeVerifier,
|
|
2956
3287
|
clientId,
|
|
2957
3288
|
clientSecret,
|
|
3289
|
+
clientAuthMethod,
|
|
2958
3290
|
tokenEndpoint: metadata.token_endpoint,
|
|
2959
3291
|
redirectUri,
|
|
2960
3292
|
createdAt: Date.now(),
|
|
@@ -3125,54 +3457,50 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
3125
3457
|
|
|
3126
3458
|
const status = await ref.authStatus(name);
|
|
3127
3459
|
const security = status.security;
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3130
|
-
? (
|
|
3131
|
-
security as {
|
|
3132
|
-
flows?: Record<
|
|
3133
|
-
string,
|
|
3134
|
-
{ tokenUrl?: string; refreshUrl?: string }
|
|
3135
|
-
>;
|
|
3136
|
-
}
|
|
3137
|
-
).flows
|
|
3138
|
-
: undefined;
|
|
3139
|
-
const authCodeFlow = flows?.authorizationCode;
|
|
3140
|
-
const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
|
|
3141
|
-
if (!tokenUrl) return null;
|
|
3142
|
-
|
|
3143
|
-
const oauthClient = await resolveOAuthClient({ name, entry, security });
|
|
3144
|
-
if (!oauthClient) return null;
|
|
3145
|
-
const { clientId, clientSecret } = oauthClient;
|
|
3146
|
-
|
|
3147
|
-
// POST to the token endpoint with grant_type=refresh_token
|
|
3148
|
-
const body = new URLSearchParams({
|
|
3149
|
-
grant_type: "refresh_token",
|
|
3150
|
-
refresh_token: refreshToken,
|
|
3151
|
-
client_id: clientId,
|
|
3460
|
+
const metadata = await resolveOAuthMetadataFromSecurity(security, {
|
|
3461
|
+
serverUrl: entry.url,
|
|
3152
3462
|
});
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3463
|
+
const tokenEndpoint = metadata?.token_endpoint;
|
|
3464
|
+
if (!tokenEndpoint) return null;
|
|
3465
|
+
|
|
3466
|
+
const oauthClient = await resolveOAuthClient({
|
|
3467
|
+
name,
|
|
3468
|
+
entry,
|
|
3469
|
+
security,
|
|
3470
|
+
metadata,
|
|
3161
3471
|
});
|
|
3472
|
+
if (!oauthClient) return null;
|
|
3162
3473
|
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3474
|
+
const clientAuthMethod = resolveClientAuthMethod(security, metadata);
|
|
3475
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
3476
|
+
let tokens: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
3477
|
+
try {
|
|
3478
|
+
tokens = await refreshAccessToken(
|
|
3479
|
+
tokenEndpoint,
|
|
3480
|
+
{
|
|
3481
|
+
refreshToken,
|
|
3482
|
+
clientId: oauthClient.clientId,
|
|
3483
|
+
clientSecret: oauthClient.clientSecret,
|
|
3484
|
+
clientAuthMethod,
|
|
3485
|
+
},
|
|
3486
|
+
fetchFn,
|
|
3487
|
+
);
|
|
3488
|
+
} catch {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3168
3491
|
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3492
|
+
await storeRefSecret(name, "access_token", tokens.accessToken);
|
|
3493
|
+
if (tokens.refreshToken) {
|
|
3494
|
+
await storeRefSecret(name, "refresh_token", tokens.refreshToken);
|
|
3495
|
+
}
|
|
3496
|
+
if (tokens.expiresIn) {
|
|
3497
|
+
const expiresAt = new Date(
|
|
3498
|
+
Date.now() + tokens.expiresIn * 1000,
|
|
3499
|
+
).toISOString();
|
|
3500
|
+
await storeRefSecret(name, "expires_at", expiresAt);
|
|
3173
3501
|
}
|
|
3174
3502
|
|
|
3175
|
-
return { accessToken:
|
|
3503
|
+
return { accessToken: tokens.accessToken };
|
|
3176
3504
|
},
|
|
3177
3505
|
};
|
|
3178
3506
|
|
|
@@ -3193,13 +3521,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
3193
3521
|
throw new Error(`No pending OAuth flow for state "${params.state}".`);
|
|
3194
3522
|
}
|
|
3195
3523
|
|
|
3196
|
-
const
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3524
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
3525
|
+
const tokens = await exchangeCodeForTokens(
|
|
3526
|
+
pending.tokenEndpoint,
|
|
3527
|
+
{
|
|
3528
|
+
code: params.code,
|
|
3529
|
+
codeVerifier: pending.codeVerifier,
|
|
3530
|
+
clientId: pending.clientId,
|
|
3531
|
+
clientSecret: pending.clientSecret,
|
|
3532
|
+
redirectUri: pending.redirectUri,
|
|
3533
|
+
clientAuthMethod: pending.clientAuthMethod,
|
|
3534
|
+
},
|
|
3535
|
+
fetchFn,
|
|
3536
|
+
);
|
|
3203
3537
|
|
|
3204
3538
|
await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
|
|
3205
3539
|
if (tokens.refreshToken) {
|
package/src/index.ts
CHANGED
|
@@ -352,7 +352,10 @@ export {
|
|
|
352
352
|
exchangeCodeForTokens,
|
|
353
353
|
refreshAccessToken as refreshMcpAccessToken,
|
|
354
354
|
} from "./mcp-client.js";
|
|
355
|
-
export type {
|
|
355
|
+
export type {
|
|
356
|
+
OAuthClientAuthMethod,
|
|
357
|
+
OAuthServerMetadata,
|
|
358
|
+
} from "./mcp-client.js";
|
|
356
359
|
|
|
357
360
|
// ============================================
|
|
358
361
|
// Serialized Agent Definitions
|