@slashfi/agents-sdk 0.26.2 → 0.27.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.
Files changed (50) hide show
  1. package/dist/agent-definitions/config.d.ts +44 -0
  2. package/dist/agent-definitions/config.d.ts.map +1 -0
  3. package/dist/agent-definitions/config.js +234 -0
  4. package/dist/agent-definitions/config.js.map +1 -0
  5. package/dist/cjs/agent-definitions/config.js +237 -0
  6. package/dist/cjs/agent-definitions/config.js.map +1 -0
  7. package/dist/cjs/codegen.js +27 -2
  8. package/dist/cjs/codegen.js.map +1 -1
  9. package/dist/cjs/index.js +21 -3
  10. package/dist/cjs/index.js.map +1 -1
  11. package/dist/cjs/mcp-client.js +159 -0
  12. package/dist/cjs/mcp-client.js.map +1 -0
  13. package/dist/cjs/pkce.js +49 -0
  14. package/dist/cjs/pkce.js.map +1 -0
  15. package/dist/cjs/registry-consumer.js +217 -2
  16. package/dist/cjs/registry-consumer.js.map +1 -1
  17. package/dist/cjs/server.js +33 -2
  18. package/dist/cjs/server.js.map +1 -1
  19. package/dist/codegen.d.ts +4 -0
  20. package/dist/codegen.d.ts.map +1 -1
  21. package/dist/codegen.js +27 -2
  22. package/dist/codegen.js.map +1 -1
  23. package/dist/index.d.ts +7 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcp-client.d.ts +87 -0
  28. package/dist/mcp-client.d.ts.map +1 -0
  29. package/dist/mcp-client.js +152 -0
  30. package/dist/mcp-client.js.map +1 -0
  31. package/dist/pkce.d.ts +29 -0
  32. package/dist/pkce.d.ts.map +1 -0
  33. package/dist/pkce.js +44 -0
  34. package/dist/pkce.js.map +1 -0
  35. package/dist/registry-consumer.d.ts +4 -0
  36. package/dist/registry-consumer.d.ts.map +1 -1
  37. package/dist/registry-consumer.js +216 -2
  38. package/dist/registry-consumer.js.map +1 -1
  39. package/dist/server.d.ts +13 -0
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +33 -2
  42. package/dist/server.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/agent-definitions/config.ts +318 -0
  45. package/src/codegen.ts +33 -1
  46. package/src/index.ts +34 -2
  47. package/src/mcp-client.ts +230 -0
  48. package/src/pkce.ts +54 -0
  49. package/src/registry-consumer.ts +257 -2
  50. package/src/server.ts +49 -2
package/src/pkce.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * PKCE (Proof Key for Code Exchange) utilities.
3
+ *
4
+ * RFC 7636 — used by MCP client OAuth flows to prevent
5
+ * authorization code interception. The code_verifier stays
6
+ * server-side; only the code_challenge is sent through the browser.
7
+ *
8
+ * This ensures auth codes are useless even if they leak into
9
+ * agent context or logs.
10
+ */
11
+
12
+ /**
13
+ * Generate a cryptographically random code_verifier.
14
+ * RFC 7636 §4.1: 43–128 characters from [A-Z, a-z, 0-9, -, ., _, ~]
15
+ */
16
+ export function generateCodeVerifier(length = 64): string {
17
+ const bytes = crypto.getRandomValues(new Uint8Array(length));
18
+ return base64urlEncode(bytes).slice(0, length);
19
+ }
20
+
21
+ /**
22
+ * Generate code_challenge from a code_verifier using S256.
23
+ * RFC 7636 §4.2: BASE64URL(SHA256(code_verifier))
24
+ */
25
+ export async function generateCodeChallenge(
26
+ verifier: string,
27
+ ): Promise<string> {
28
+ const encoder = new TextEncoder();
29
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(verifier));
30
+ return base64urlEncode(new Uint8Array(digest));
31
+ }
32
+
33
+ /**
34
+ * Generate a PKCE pair (verifier + challenge) in one call.
35
+ */
36
+ export async function generatePkcePair(): Promise<{
37
+ codeVerifier: string;
38
+ codeChallenge: string;
39
+ codeChallengeMethod: "S256";
40
+ }> {
41
+ const codeVerifier = generateCodeVerifier();
42
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
43
+ return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
44
+ }
45
+
46
+ // ============================================
47
+ // Helpers
48
+ // ============================================
49
+
50
+ /** Base64url encode without padding (RFC 4648 §5) */
51
+ function base64urlEncode(bytes: Uint8Array): string {
52
+ const binStr = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
53
+ return btoa(binStr).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
54
+ }
@@ -42,6 +42,18 @@ import {
42
42
  normalizeRef,
43
43
  normalizeRegistry,
44
44
  } from "./define-config.js";
45
+ // TODO: wire discoverOAuthMetadata from ./mcp-client.js into MCP server auth negotiation
46
+
47
+ // ============================================
48
+ // Registry Type Constants
49
+ // ============================================
50
+
51
+ /** Special registry type: connect directly to an MCP server */
52
+ export const REGISTRY_TYPE_MCP = "mcp";
53
+ /** Special registry type: raw HTTP/REST API */
54
+ export const REGISTRY_TYPE_HTTPS = "https";
55
+ /** Built-in registry types that bypass normal registry resolution */
56
+ const DIRECT_REGISTRY_TYPES = new Set([REGISTRY_TYPE_MCP, REGISTRY_TYPE_HTTPS]);
45
57
 
46
58
  // ============================================
47
59
  // Registry Discovery Types
@@ -140,6 +152,190 @@ async function defaultSecretResolver(
140
152
  }
141
153
  }
142
154
 
155
+ // ============================================
156
+ // Direct MCP Resolution
157
+ // ============================================
158
+
159
+ /**
160
+ * List tools from a direct MCP server (registry type: 'mcp').
161
+ * Connects via JSON-RPC, does MCP initialize handshake, then tools/list.
162
+ */
163
+ async function listFromMcpServer(
164
+ url: string,
165
+ auth: { token?: string; headers?: Record<string, string> },
166
+ fetchFn: typeof globalThis.fetch,
167
+ ): Promise<AgentListing[]> {
168
+ const serverUrl = url.replace(/\/$/, "");
169
+
170
+ const headers: Record<string, string> = {
171
+ "Content-Type": "application/json",
172
+ ...(auth.headers ?? {}),
173
+ };
174
+ if (auth.token) {
175
+ headers.Authorization = `Bearer ${auth.token}`;
176
+ }
177
+
178
+ let reqId = 0;
179
+ async function rpc(method: string, params?: Record<string, unknown>) {
180
+ const res = await fetchFn(serverUrl, {
181
+ method: "POST",
182
+ headers,
183
+ body: JSON.stringify({
184
+ jsonrpc: "2.0",
185
+ id: ++reqId,
186
+ method,
187
+ ...(params && { params }),
188
+ }),
189
+ });
190
+ if (!res.ok) {
191
+ throw new Error(`MCP call to ${serverUrl} failed: ${res.status}`);
192
+ }
193
+ const json = (await res.json()) as { result?: unknown; error?: { message: string } };
194
+ if (json.error) {
195
+ throw new Error(`MCP RPC error: ${json.error.message}`);
196
+ }
197
+ return json.result;
198
+ }
199
+
200
+ // Initialize handshake
201
+ const initResult = (await rpc("initialize", {
202
+ protocolVersion: "2024-11-05",
203
+ capabilities: {},
204
+ clientInfo: { name: "agents-sdk-consumer", version: "1.0.0" },
205
+ })) as { serverInfo?: { name?: string }; capabilities?: { registry?: unknown } };
206
+
207
+ // Send initialized notification
208
+ await rpc("notifications/initialized").catch(() => {});
209
+
210
+ // List tools
211
+ const toolsResult = (await rpc("tools/list")) as {
212
+ tools?: Array<{ name: string; description?: string }>;
213
+ };
214
+
215
+ const serverName = initResult?.serverInfo?.name ?? new URL(serverUrl).hostname;
216
+
217
+ // Return as a single agent listing with all tools
218
+ return [{
219
+ path: serverName,
220
+ description: `MCP server at ${serverUrl}`,
221
+ publisher: serverName,
222
+ tools: toolsResult?.tools ?? [],
223
+ requiresAuth: false,
224
+ }];
225
+ }
226
+
227
+ /**
228
+ * Call a tool on a direct MCP server.
229
+ */
230
+ async function callMcpTool(
231
+ url: string,
232
+ toolName: string,
233
+ params: Record<string, unknown>,
234
+ auth: { token?: string; headers?: Record<string, string> },
235
+ fetchFn: typeof globalThis.fetch,
236
+ ): Promise<unknown> {
237
+ const serverUrl = url.replace(/\/$/, "");
238
+ const headers: Record<string, string> = {
239
+ "Content-Type": "application/json",
240
+ ...(auth.headers ?? {}),
241
+ };
242
+ if (auth.token) {
243
+ headers.Authorization = `Bearer ${auth.token}`;
244
+ }
245
+
246
+ const res = await fetchFn(serverUrl, {
247
+ method: "POST",
248
+ headers,
249
+ body: JSON.stringify({
250
+ jsonrpc: "2.0",
251
+ id: 1,
252
+ method: "tools/call",
253
+ params: { name: toolName, arguments: params },
254
+ }),
255
+ });
256
+ if (!res.ok) {
257
+ throw new Error(`MCP tool call failed: ${res.status}`);
258
+ }
259
+ const json = (await res.json()) as { result?: unknown; error?: { message: string } };
260
+ if (json.error) {
261
+ throw new Error(`MCP RPC error: ${json.error.message}`);
262
+ }
263
+
264
+ // Extract text content
265
+ const result = json.result as { content?: Array<{ type: string; text?: string }> };
266
+ if (result?.content) {
267
+ const textItem = result.content.find((c) => c.type === "text");
268
+ if (textItem?.text) {
269
+ try { return JSON.parse(textItem.text); } catch { return textItem.text; }
270
+ }
271
+ }
272
+ return result;
273
+ }
274
+
275
+ // ============================================
276
+ // Direct HTTPS Resolution
277
+ // ============================================
278
+
279
+ /**
280
+ * List available operations from an HTTPS API (registry type: 'https').
281
+ * Returns a single generic 'call' tool since we can't auto-discover REST endpoints
282
+ * without an OpenAPI spec.
283
+ */
284
+ function listFromHttpsApi(url: string): AgentListing[] {
285
+ const hostname = new URL(url).hostname;
286
+ return [{
287
+ path: hostname,
288
+ description: `REST API at ${url}`,
289
+ publisher: hostname,
290
+ tools: [{
291
+ name: "call",
292
+ description: "Make an HTTP request to the API. Params: method, path, body, headers.",
293
+ }],
294
+ requiresAuth: false,
295
+ }];
296
+ }
297
+
298
+ /**
299
+ * Call an HTTPS API (registry type: 'https').
300
+ * Generic HTTP proxy with auth injection.
301
+ */
302
+ async function callHttpsTool(
303
+ baseUrl: string,
304
+ _toolName: string,
305
+ params: Record<string, unknown>,
306
+ auth: { token?: string; headers?: Record<string, string> },
307
+ fetchFn: typeof globalThis.fetch,
308
+ ): Promise<unknown> {
309
+ const method = (params.method as string) ?? "GET";
310
+ const path = (params.path as string) ?? "";
311
+ const body = params.body as Record<string, unknown> | undefined;
312
+ const extraHeaders = (params.headers as Record<string, string>) ?? {};
313
+
314
+ const url = `${baseUrl.replace(/\/$/, "")}${path}`;
315
+ const headers: Record<string, string> = {
316
+ ...extraHeaders,
317
+ ...(auth.headers ?? {}),
318
+ };
319
+ if (auth.token) {
320
+ headers.Authorization = `Bearer ${auth.token}`;
321
+ }
322
+ if (body) {
323
+ headers["Content-Type"] = "application/json";
324
+ }
325
+
326
+ const res = await fetchFn(url, {
327
+ method,
328
+ headers,
329
+ ...(body && { body: JSON.stringify(body) }),
330
+ });
331
+
332
+ const contentType = res.headers.get("content-type") ?? "";
333
+ if (contentType.includes("json")) {
334
+ return res.json();
335
+ }
336
+ return res.text();
337
+ }
338
+
143
339
  // ============================================
144
340
  // Consumer Options
145
341
  // ============================================
@@ -320,10 +516,42 @@ export async function createRegistryConsumer(
320
516
  // Build the consumer
321
517
  const consumer: RegistryConsumer = {
322
518
  async list(): Promise<AgentListing[]> {
323
- const results = await Promise.allSettled(
519
+ // Collect from standard registries
520
+ const registryResults = await Promise.allSettled(
324
521
  resolvedRegistries.map(listFromRegistry),
325
522
  );
326
- return results.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
523
+ const listings = registryResults.flatMap((r) =>
524
+ r.status === "fulfilled" ? r.value : [],
525
+ );
526
+
527
+ // Also collect from direct MCP/HTTPS refs
528
+ for (const ref of resolvedRefs) {
529
+ if (!DIRECT_REGISTRY_TYPES.has(ref.registry)) continue;
530
+ const refEntry = (config.refs ?? []).find((r) => {
531
+ const n = normalizeRef(r);
532
+ return n.name === ref.name;
533
+ });
534
+ const url =
535
+ typeof refEntry === "object" ? refEntry?.url : undefined;
536
+ if (!url) continue;
537
+
538
+ try {
539
+ if (ref.registry === REGISTRY_TYPE_MCP) {
540
+ const mcpListings = await listFromMcpServer(
541
+ url,
542
+ { token: options.token },
543
+ fetchFn,
544
+ );
545
+ listings.push(...mcpListings);
546
+ } else if (ref.registry === REGISTRY_TYPE_HTTPS) {
547
+ listings.push(...listFromHttpsApi(url));
548
+ }
549
+ } catch {
550
+ // Skip unreachable direct refs during list
551
+ }
552
+ }
553
+
554
+ return listings;
327
555
  },
328
556
 
329
557
  refs(): ResolvedRef[] {
@@ -346,6 +574,33 @@ export async function createRegistryConsumer(
346
574
  );
347
575
  }
348
576
 
577
+ // Direct MCP ref — bypass registry, call MCP server directly
578
+ if (ref.registry === REGISTRY_TYPE_MCP) {
579
+ const refEntry = (config.refs ?? []).find((r) => {
580
+ const n = normalizeRef(r);
581
+ return n.name === ref.name;
582
+ });
583
+ const url = typeof refEntry === "object" ? refEntry?.url : undefined;
584
+ if (!url) {
585
+ throw new Error(`MCP ref "${refName}" has no url`);
586
+ }
587
+ return callMcpTool(url, tool, params, { token: options.token }, fetchFn);
588
+ }
589
+
590
+ // Direct HTTPS ref — bypass registry, call REST API directly
591
+ if (ref.registry === REGISTRY_TYPE_HTTPS) {
592
+ const refEntry = (config.refs ?? []).find((r) => {
593
+ const n = normalizeRef(r);
594
+ return n.name === ref.name;
595
+ });
596
+ const url = typeof refEntry === "object" ? refEntry?.url : undefined;
597
+ if (!url) {
598
+ throw new Error(`HTTPS ref "${refName}" has no url`);
599
+ }
600
+ return callHttpsTool(url, tool, params, { token: options.token }, fetchFn);
601
+ }
602
+
603
+ // Standard registry ref
349
604
  const registry = resolvedRegistries.find(
350
605
  (r) => r.url === ref.registry || r.name === ref.registry,
351
606
  );
package/src/server.ts CHANGED
@@ -120,6 +120,19 @@ export interface AgentServerOptions {
120
120
  keyStore?: import("./key-manager.js").KeyStore;
121
121
  /** OIDC provider for user sign-in (authorization code flow) */
122
122
  oidcProvider?: OIDCProviderConfig;
123
+ /**
124
+ * Registry capabilities — advertised in MCP initialize response.
125
+ * When set, this server identifies as an agent registry (superset of MCP).
126
+ * Consumers use this to differentiate `registry` type from plain `mcp`.
127
+ */
128
+ registry?: {
129
+ /** Registry protocol version */
130
+ version?: string;
131
+ /** Feature flags (e.g., 'shared-oauth', 'agent-listing') */
132
+ features?: string[];
133
+ /** OAuth callback URL for shared OAuth flows */
134
+ oauthCallbackUrl?: string;
135
+ };
123
136
  }
124
137
 
125
138
  export interface AgentServer {
@@ -518,7 +531,16 @@ export function createAgentServer(
518
531
  case "initialize":
519
532
  return jsonRpcSuccess(request.id, {
520
533
  protocolVersion: "2024-11-05",
521
- capabilities: { tools: { listChanged: false } },
534
+ capabilities: {
535
+ tools: { listChanged: false },
536
+ ...(options.registry && {
537
+ registry: {
538
+ version: options.registry.version ?? "1.0",
539
+ ...(options.registry.features && { features: options.registry.features }),
540
+ ...(options.registry.oauthCallbackUrl && { oauthCallbackUrl: options.registry.oauthCallbackUrl }),
541
+ },
542
+ }),
543
+ },
522
544
  serverInfo: { name: serverName, version: serverVersion },
523
545
  });
524
546
 
@@ -1178,7 +1200,7 @@ export function createAgentServer(
1178
1200
  return cors ? addCors(res) : res;
1179
1201
  }
1180
1202
 
1181
- // ── GET /.well-known/configuration → Server discovery ──
1203
+ // ── GET /.well-known/configuration → Server discovery (deprecated, use MCP initialize capabilities) ──
1182
1204
  if (path === "/.well-known/configuration" && req.method === "GET") {
1183
1205
  const baseUrl = resolveBaseUrl(req);
1184
1206
  const res = jsonResponse({
@@ -1196,6 +1218,31 @@ export function createAgentServer(
1196
1218
  return cors ? addCors(res) : res;
1197
1219
  }
1198
1220
 
1221
+ // ── GET /.well-known/oauth-authorization-server → OAuth Server Metadata (RFC 8414) ──
1222
+ // Only exposed when the server requires auth (private registries).
1223
+ // Public registries (e.g. registry.slash.com) skip this entirely.
1224
+ if (
1225
+ path === "/.well-known/oauth-authorization-server" &&
1226
+ req.method === "GET" &&
1227
+ (options.registry?.oauthCallbackUrl || serverSigningKeys.length > 0)
1228
+ ) {
1229
+ const baseUrl = resolveBaseUrl(req);
1230
+ const res = jsonResponse({
1231
+ issuer: baseUrl,
1232
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
1233
+ token_endpoint: `${baseUrl}/oauth/token`,
1234
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
1235
+ response_types_supported: ["code"],
1236
+ grant_types_supported: ["authorization_code", "client_credentials", "jwt_exchange"],
1237
+ code_challenge_methods_supported: ["S256"],
1238
+ token_endpoint_auth_methods_supported: ["client_secret_post", "none"],
1239
+ ...(options.registry?.oauthCallbackUrl && {
1240
+ registration_endpoint: `${baseUrl}/oauth/register`,
1241
+ }),
1242
+ });
1243
+ return cors ? addCors(res) : res;
1244
+ }
1245
+
1199
1246
  // ── GET /list → List agents (legacy endpoint) ──
1200
1247
  if (path === "/list" && req.method === "GET") {
1201
1248
  const agents = registry.list();