@slashfi/agents-sdk 0.76.0 → 0.77.1
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-tools.d.ts +2 -2
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +9 -18
- package/dist/adk-tools.js.map +1 -1
- package/dist/adk.js +190 -120
- package/dist/adk.js.map +1 -1
- package/dist/agent-definitions/config.d.ts.map +1 -1
- package/dist/agent-definitions/config.js +12 -14
- package/dist/agent-definitions/config.js.map +1 -1
- package/dist/cjs/adk-tools.js +9 -18
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/agent-definitions/config.js +12 -14
- package/dist/cjs/agent-definitions/config.js.map +1 -1
- package/dist/cjs/config-store.js +527 -30
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js +5 -7
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/materialize.js +1 -1
- package/dist/cjs/materialize.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 +69 -11
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/config-store.d.ts +39 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +528 -31
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +65 -18
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +5 -7
- package/dist/define-config.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/materialize.js +1 -1
- package/dist/materialize.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 +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +69 -11
- package/dist/registry-consumer.js.map +1 -1
- package/dist/validate.d.ts +8 -8
- package/package.json +1 -1
- package/src/adk-tools.ts +11 -18
- package/src/adk.ts +78 -11
- package/src/agent-definitions/config.ts +15 -16
- package/src/config-store.test.ts +212 -0
- package/src/config-store.ts +615 -37
- package/src/consumer.test.ts +7 -7
- package/src/define-config.ts +69 -20
- package/src/index.ts +1 -0
- package/src/materialize.ts +1 -1
- package/src/mcp-client.ts +121 -0
- package/src/ref-naming.test.ts +115 -90
- package/src/registry-consumer.ts +75 -13
package/src/consumer.test.ts
CHANGED
|
@@ -111,7 +111,7 @@ describe("Registry Consumer E2E", () => {
|
|
|
111
111
|
registries: [`http://localhost:${PORT}`],
|
|
112
112
|
refs: [
|
|
113
113
|
{ ref: "@math" },
|
|
114
|
-
{ ref: "@echo",
|
|
114
|
+
{ ref: "@echo", name: "my-echo", config: { greeting: "hello" } },
|
|
115
115
|
],
|
|
116
116
|
};
|
|
117
117
|
|
|
@@ -151,12 +151,12 @@ describe("Registry Consumer E2E", () => {
|
|
|
151
151
|
);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test("multi-instance refs with
|
|
154
|
+
test("multi-instance refs with name aliases", async () => {
|
|
155
155
|
const config = {
|
|
156
156
|
registries: [`http://localhost:${PORT}`],
|
|
157
157
|
refs: [
|
|
158
|
-
{ ref: "@echo",
|
|
159
|
-
{ ref: "@echo",
|
|
158
|
+
{ ref: "@echo", name: "echo-1", config: { prefix: "first" } },
|
|
159
|
+
{ ref: "@echo", name: "echo-2", config: { prefix: "second" } },
|
|
160
160
|
],
|
|
161
161
|
};
|
|
162
162
|
|
|
@@ -240,10 +240,10 @@ describe("normalizeRef", () => {
|
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
-
test("object ref with
|
|
243
|
+
test("object ref with name", () => {
|
|
244
244
|
const result = normalizeRef({
|
|
245
245
|
ref: "postgres",
|
|
246
|
-
|
|
246
|
+
name: "prod-db",
|
|
247
247
|
config: { url: "https://twin.slash.com/secrets/db" },
|
|
248
248
|
});
|
|
249
249
|
expect(result.ref).toBe("postgres");
|
|
@@ -253,7 +253,7 @@ describe("normalizeRef", () => {
|
|
|
253
253
|
});
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
-
test("object ref without
|
|
256
|
+
test("object ref without name uses ref as name", () => {
|
|
257
257
|
const result = normalizeRef({ ref: "github" });
|
|
258
258
|
expect(result.name).toBe("github");
|
|
259
259
|
});
|
package/src/define-config.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* ],
|
|
17
17
|
* refs: [
|
|
18
18
|
* 'notion',
|
|
19
|
-
* { ref: 'postgres',
|
|
20
|
-
* { ref: 'postgres',
|
|
19
|
+
* { ref: 'postgres', name: 'prod-db', config: { url: 'https://twin.slash.com/secrets/crdb-url' } },
|
|
20
|
+
* { ref: 'postgres', name: 'staging', config: { url: 'https://twin.slash.com/secrets/staging-url' } },
|
|
21
21
|
* ],
|
|
22
22
|
* });
|
|
23
23
|
* ```
|
|
@@ -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
|
// ============================================
|
|
@@ -100,14 +158,10 @@ export type RefEntry = {
|
|
|
100
158
|
|
|
101
159
|
/**
|
|
102
160
|
* Local identifier for this ref. Used by all operations
|
|
103
|
-
* (call/remove/auth/update/…) to look up the entry.
|
|
104
|
-
*
|
|
105
|
-
* common case "one local instance per agent" requires only
|
|
106
|
-
* `{ ref: 'notion', ... }`. Set `name` to a different value only
|
|
107
|
-
* when you need multiple local instances of the same remote
|
|
108
|
-
* agent (e.g. `{ ref: 'notion', name: 'work-notion' }`).
|
|
161
|
+
* (call/remove/auth/update/…) to look up the entry. Add paths
|
|
162
|
+
* default this to `ref` when omitted.
|
|
109
163
|
*/
|
|
110
|
-
name
|
|
164
|
+
name: string;
|
|
111
165
|
|
|
112
166
|
/** Connection scheme */
|
|
113
167
|
scheme?: 'mcp' | 'https' | 'registry';
|
|
@@ -115,12 +169,6 @@ export type RefEntry = {
|
|
|
115
169
|
/** Direct URL to the agent (e.g. https://mcp.notion.com/mcp) */
|
|
116
170
|
url?: string;
|
|
117
171
|
|
|
118
|
-
/**
|
|
119
|
-
* @deprecated Use `name` instead. `as` is preserved for reading
|
|
120
|
-
* old consumer-config.json files; new writes emit `name`.
|
|
121
|
-
*/
|
|
122
|
-
as?: string;
|
|
123
|
-
|
|
124
172
|
/** Per-instance config (headers, secrets, etc. — values support {{secret-uri}} templates) */
|
|
125
173
|
config?: RefConfig;
|
|
126
174
|
|
|
@@ -131,6 +179,9 @@ export type RefEntry = {
|
|
|
131
179
|
status?: 'active' | 'inactive' | 'error';
|
|
132
180
|
};
|
|
133
181
|
|
|
182
|
+
/** Input accepted by add paths. `name` defaults to `ref` when omitted. */
|
|
183
|
+
export type RefAddInput = Omit<RefEntry, "name"> & { name?: string };
|
|
184
|
+
|
|
134
185
|
// ============================================
|
|
135
186
|
// Consumer Config
|
|
136
187
|
// ============================================
|
|
@@ -201,15 +252,13 @@ export interface ResolvedConfig {
|
|
|
201
252
|
/**
|
|
202
253
|
* Normalize a ref entry to its full form.
|
|
203
254
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* consistent with the `ref.add({ ref, name })` contract while still
|
|
207
|
-
* reading old `{ ref, as }` entries from pre-0.74 consumer-config.json.
|
|
255
|
+
* Add paths default `name` to `ref` before writing. `normalizeRef` keeps the
|
|
256
|
+
* same invariant for in-memory/test configs that omit `name`.
|
|
208
257
|
*/
|
|
209
258
|
export function normalizeRef(entry: RefEntry): ResolvedRef {
|
|
210
259
|
return {
|
|
211
260
|
...entry,
|
|
212
|
-
name: entry.name ?? entry.
|
|
261
|
+
name: entry.name ?? entry.ref,
|
|
213
262
|
config: entry.config ?? {},
|
|
214
263
|
};
|
|
215
264
|
}
|
package/src/index.ts
CHANGED
package/src/materialize.ts
CHANGED
|
@@ -326,7 +326,7 @@ export async function syncAllRefs(
|
|
|
326
326
|
for (const refEntry of refs) {
|
|
327
327
|
const name = typeof refEntry === "string"
|
|
328
328
|
? refEntry
|
|
329
|
-
: (refEntry as any).
|
|
329
|
+
: (refEntry as any).name ?? (refEntry as any).ref;
|
|
330
330
|
if (!name) continue;
|
|
331
331
|
if (opts?.filter && name !== opts.filter) continue;
|
|
332
332
|
names.push(name);
|
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/ref-naming.test.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the `ref` naming contract introduced in 0.74:
|
|
3
3
|
*
|
|
4
|
-
* - `
|
|
5
|
-
*
|
|
6
|
-
* - `
|
|
7
|
-
*
|
|
8
|
-
* in either shape.
|
|
9
|
-
* - `createRefTool` (the adk-tools.ts MCP tool) drops `as` from its
|
|
10
|
-
* schema and defaults `ref` to `name` on add, so "Add a ref called X"
|
|
11
|
-
* via an LLM that picks either field lands on the same stored
|
|
12
|
-
* `{ ref: 'X' }` entry.
|
|
4
|
+
* - `name` is the local identifier for every stored ref entry.
|
|
5
|
+
* - Add paths default `name` to `ref` when omitted.
|
|
6
|
+
* - `as` is not part of the stored or public ref shape.
|
|
7
|
+
* - Adding a duplicate `name` is a loud error, not a replace.
|
|
13
8
|
*/
|
|
14
9
|
|
|
15
10
|
import { describe, expect, test } from "bun:test";
|
|
@@ -42,7 +37,7 @@ async function readJson<T = unknown>(fs: FsStore, path: string): Promise<T> {
|
|
|
42
37
|
// ─── RefEntry: name field on write ───────────────────────────────────
|
|
43
38
|
|
|
44
39
|
describe("ref.add — identifier field", () => {
|
|
45
|
-
test("single-instance case stores
|
|
40
|
+
test("single-instance case stores explicit `name` equal to `ref`", async () => {
|
|
46
41
|
const fs = createMemoryFs();
|
|
47
42
|
const adk = createAdk(fs);
|
|
48
43
|
|
|
@@ -58,8 +53,7 @@ describe("ref.add — identifier field", () => {
|
|
|
58
53
|
);
|
|
59
54
|
expect(parsed.refs).toHaveLength(1);
|
|
60
55
|
expect(parsed.refs[0].ref).toBe("test-ref");
|
|
61
|
-
expect(parsed.refs[0].name).
|
|
62
|
-
expect(parsed.refs[0].as).toBeUndefined();
|
|
56
|
+
expect(parsed.refs[0].name).toBe("test-ref");
|
|
63
57
|
});
|
|
64
58
|
|
|
65
59
|
test("aliasing case stores both `ref` and `name`", async () => {
|
|
@@ -79,14 +73,40 @@ describe("ref.add — identifier field", () => {
|
|
|
79
73
|
);
|
|
80
74
|
expect(parsed.refs[0].ref).toBe("notion");
|
|
81
75
|
expect(parsed.refs[0].name).toBe("work-notion");
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("adding a duplicate name rejects instead of replacing", async () => {
|
|
79
|
+
const fs = createMemoryFs();
|
|
80
|
+
const adk = createAdk(fs);
|
|
81
|
+
|
|
82
|
+
await adk.ref.add({
|
|
83
|
+
ref: "notion",
|
|
84
|
+
name: "work",
|
|
85
|
+
scheme: "mcp",
|
|
86
|
+
url: "http://localhost:12345",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await expect(
|
|
90
|
+
adk.ref.add({
|
|
91
|
+
ref: "linear",
|
|
92
|
+
name: "work",
|
|
93
|
+
scheme: "mcp",
|
|
94
|
+
url: "http://localhost:12346",
|
|
95
|
+
}),
|
|
96
|
+
).rejects.toThrow(/already exists/);
|
|
97
|
+
|
|
98
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
99
|
+
fs,
|
|
100
|
+
"consumer-config.json",
|
|
101
|
+
);
|
|
102
|
+
expect(parsed.refs).toHaveLength(1);
|
|
103
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
84
104
|
});
|
|
85
105
|
});
|
|
86
106
|
|
|
87
107
|
// ─── Lookup compatibility: new `name` field works end-to-end ─────────
|
|
88
108
|
|
|
89
|
-
describe("ref lookup — name/
|
|
109
|
+
describe("ref lookup — name/ref resolution", () => {
|
|
90
110
|
test("entries written with `name` are findable by `name`", async () => {
|
|
91
111
|
const fs = createMemoryFs();
|
|
92
112
|
const adk = createAdk(fs);
|
|
@@ -119,9 +139,7 @@ describe("ref lookup — name/as/ref resolution", () => {
|
|
|
119
139
|
expect(refs[0]?.name).toBe("work-notion");
|
|
120
140
|
});
|
|
121
141
|
|
|
122
|
-
test("
|
|
123
|
-
// Simulate a consumer-config.json produced by a pre-0.74 client that
|
|
124
|
-
// still writes the `as` field. The read path must still resolve it.
|
|
142
|
+
test("entries without `name` normalize to `ref`", async () => {
|
|
125
143
|
const fs = createMemoryFs();
|
|
126
144
|
await fs.writeFile(
|
|
127
145
|
"consumer-config.json",
|
|
@@ -129,7 +147,6 @@ describe("ref lookup — name/as/ref resolution", () => {
|
|
|
129
147
|
refs: [
|
|
130
148
|
{
|
|
131
149
|
ref: "notion",
|
|
132
|
-
as: "work-notion",
|
|
133
150
|
scheme: "mcp",
|
|
134
151
|
url: "http://localhost:12345",
|
|
135
152
|
},
|
|
@@ -138,56 +155,26 @@ describe("ref lookup — name/as/ref resolution", () => {
|
|
|
138
155
|
);
|
|
139
156
|
const adk = createAdk(fs);
|
|
140
157
|
|
|
141
|
-
const
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
144
|
-
expect(entry?.as).toBe("work-notion");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("when both `name` and `as` are present, `name` wins", async () => {
|
|
148
|
-
const fs = createMemoryFs();
|
|
149
|
-
await fs.writeFile(
|
|
150
|
-
"consumer-config.json",
|
|
151
|
-
JSON.stringify({
|
|
152
|
-
refs: [
|
|
153
|
-
{
|
|
154
|
-
ref: "notion",
|
|
155
|
-
name: "new-identifier",
|
|
156
|
-
as: "legacy-identifier",
|
|
157
|
-
scheme: "mcp",
|
|
158
|
-
url: "http://localhost:12345",
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
}),
|
|
162
|
-
);
|
|
163
|
-
const adk = createAdk(fs);
|
|
164
|
-
|
|
165
|
-
const byNew = await adk.ref.get("new-identifier");
|
|
166
|
-
expect(byNew).not.toBeNull();
|
|
167
|
-
expect(byNew?.ref).toBe("notion");
|
|
158
|
+
const refs = await adk.ref.list();
|
|
159
|
+
expect(refs[0]?.name).toBe("notion");
|
|
160
|
+
expect(await adk.ref.get("notion")).not.toBeNull();
|
|
168
161
|
});
|
|
169
162
|
});
|
|
170
163
|
|
|
171
|
-
// ─── ref.update:
|
|
164
|
+
// ─── ref.update: name is the only rename field ───────────────────────
|
|
172
165
|
|
|
173
|
-
describe("ref.update — name
|
|
174
|
-
test("passing `name` in updates sets name
|
|
166
|
+
describe("ref.update — name handling", () => {
|
|
167
|
+
test("passing `name` in updates sets name", async () => {
|
|
175
168
|
const fs = createMemoryFs();
|
|
176
|
-
await fs.writeFile(
|
|
177
|
-
"consumer-config.json",
|
|
178
|
-
JSON.stringify({
|
|
179
|
-
refs: [
|
|
180
|
-
{
|
|
181
|
-
ref: "notion",
|
|
182
|
-
as: "old-alias",
|
|
183
|
-
scheme: "mcp",
|
|
184
|
-
url: "http://localhost:12345",
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
169
|
const adk = createAdk(fs);
|
|
190
170
|
|
|
171
|
+
await adk.ref.add({
|
|
172
|
+
ref: "notion",
|
|
173
|
+
name: "old-alias",
|
|
174
|
+
scheme: "mcp",
|
|
175
|
+
url: "http://localhost:12345",
|
|
176
|
+
});
|
|
177
|
+
|
|
191
178
|
const ok = await adk.ref.update("old-alias", { name: "new-alias" });
|
|
192
179
|
expect(ok).toBe(true);
|
|
193
180
|
|
|
@@ -196,35 +183,29 @@ describe("ref.update — name/as handling", () => {
|
|
|
196
183
|
"consumer-config.json",
|
|
197
184
|
);
|
|
198
185
|
expect(parsed.refs[0].name).toBe("new-alias");
|
|
199
|
-
expect(parsed.refs[0].as).toBeUndefined();
|
|
200
186
|
expect(parsed.refs[0].ref).toBe("notion");
|
|
201
187
|
});
|
|
202
188
|
|
|
203
|
-
test("
|
|
189
|
+
test("renaming to an existing name rejects", async () => {
|
|
204
190
|
const fs = createMemoryFs();
|
|
205
|
-
await fs.writeFile(
|
|
206
|
-
"consumer-config.json",
|
|
207
|
-
JSON.stringify({
|
|
208
|
-
refs: [
|
|
209
|
-
{
|
|
210
|
-
ref: "notion",
|
|
211
|
-
as: "first",
|
|
212
|
-
scheme: "mcp",
|
|
213
|
-
url: "http://localhost:12345",
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
}),
|
|
217
|
-
);
|
|
218
191
|
const adk = createAdk(fs);
|
|
219
192
|
|
|
220
|
-
|
|
221
|
-
|
|
193
|
+
await adk.ref.add({
|
|
194
|
+
ref: "notion",
|
|
195
|
+
name: "first",
|
|
196
|
+
scheme: "mcp",
|
|
197
|
+
url: "http://localhost:12345",
|
|
198
|
+
});
|
|
199
|
+
await adk.ref.add({
|
|
200
|
+
ref: "linear",
|
|
201
|
+
name: "second",
|
|
202
|
+
scheme: "mcp",
|
|
203
|
+
url: "http://localhost:12346",
|
|
204
|
+
});
|
|
222
205
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"consumer-config.json",
|
|
206
|
+
await expect(adk.ref.update("first", { name: "second" })).rejects.toThrow(
|
|
207
|
+
/already exists/,
|
|
226
208
|
);
|
|
227
|
-
expect(parsed.refs[0].as).toBe("second");
|
|
228
209
|
});
|
|
229
210
|
});
|
|
230
211
|
|
|
@@ -232,10 +213,8 @@ describe("ref.update — name/as handling", () => {
|
|
|
232
213
|
//
|
|
233
214
|
// When an LLM is prompted with "Add a ref called X", its tool-call
|
|
234
215
|
// arguments can land on either `{ ref: 'X', … }` or `{ name: 'X', … }`
|
|
235
|
-
// depending on sampling.
|
|
236
|
-
//
|
|
237
|
-
// `add` handler now defaults `ref ??= name` so both paths converge on
|
|
238
|
-
// the same stored entry.
|
|
216
|
+
// depending on sampling. The `add` handler defaults the missing field so
|
|
217
|
+
// both paths converge on the same stored `{ ref: 'X', name: 'X' }` entry.
|
|
239
218
|
|
|
240
219
|
describe("ref tool — add operation defaults ref to name", () => {
|
|
241
220
|
function makeRefTool(adk: ReturnType<typeof createAdk>) {
|
|
@@ -267,8 +246,7 @@ describe("ref tool — add operation defaults ref to name", () => {
|
|
|
267
246
|
);
|
|
268
247
|
expect(parsed.refs).toHaveLength(1);
|
|
269
248
|
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
270
|
-
|
|
271
|
-
expect(parsed.refs[0].name).toBeUndefined();
|
|
249
|
+
expect(parsed.refs[0].name).toBe("test-identity-ref");
|
|
272
250
|
|
|
273
251
|
const entry = await adk.ref.get("test-identity-ref");
|
|
274
252
|
expect(entry).not.toBeNull();
|
|
@@ -295,7 +273,7 @@ describe("ref tool — add operation defaults ref to name", () => {
|
|
|
295
273
|
"consumer-config.json",
|
|
296
274
|
);
|
|
297
275
|
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
298
|
-
expect(parsed.refs[0].name).
|
|
276
|
+
expect(parsed.refs[0].name).toBe("test-identity-ref");
|
|
299
277
|
});
|
|
300
278
|
|
|
301
279
|
test("LLM sends `ref` + different `name` → stored as canonical + alias", async () => {
|
|
@@ -349,3 +327,50 @@ describe("ref tool — add operation defaults ref to name", () => {
|
|
|
349
327
|
expect(raw).toBeNull();
|
|
350
328
|
});
|
|
351
329
|
});
|
|
330
|
+
|
|
331
|
+
describe("ref tool — auth state hook", () => {
|
|
332
|
+
test("passes tool input to getAuthStateContext", async () => {
|
|
333
|
+
const authCalls: Array<{
|
|
334
|
+
name: string;
|
|
335
|
+
opts: { stateContext?: Record<string, unknown> };
|
|
336
|
+
}> = [];
|
|
337
|
+
const adk = {
|
|
338
|
+
ref: {
|
|
339
|
+
auth: async (
|
|
340
|
+
name: string,
|
|
341
|
+
opts: { stateContext?: Record<string, unknown> },
|
|
342
|
+
) => {
|
|
343
|
+
authCalls.push({ name, opts });
|
|
344
|
+
return { complete: true };
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
} as unknown as ReturnType<typeof createAdk>;
|
|
348
|
+
const tools = createAdkTools({
|
|
349
|
+
resolveScope: () => adk,
|
|
350
|
+
hooks: {
|
|
351
|
+
getAuthStateContext: async (input) => ({
|
|
352
|
+
name: input.name,
|
|
353
|
+
ref: input.ref,
|
|
354
|
+
}),
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
const refTool = tools.find((t) => t.name === "ref");
|
|
358
|
+
if (!refTool) throw new Error("ref tool not found");
|
|
359
|
+
|
|
360
|
+
await refTool.execute(
|
|
361
|
+
{
|
|
362
|
+
operation: "auth",
|
|
363
|
+
ref: "google-gmail",
|
|
364
|
+
name: "work2",
|
|
365
|
+
},
|
|
366
|
+
{} as ToolContext,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(authCalls).toHaveLength(1);
|
|
370
|
+
expect(authCalls[0]?.name).toBe("work2");
|
|
371
|
+
expect(authCalls[0]?.opts.stateContext).toEqual({
|
|
372
|
+
ref: "google-gmail",
|
|
373
|
+
name: "work2",
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|