@slashfi/agents-sdk 0.26.2 → 0.27.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/agent-definitions/config.d.ts +44 -0
- package/dist/agent-definitions/config.d.ts.map +1 -0
- package/dist/agent-definitions/config.js +234 -0
- package/dist/agent-definitions/config.js.map +1 -0
- package/dist/cjs/agent-definitions/config.js +237 -0
- package/dist/cjs/agent-definitions/config.js.map +1 -0
- package/dist/cjs/codegen.js +46 -2
- package/dist/cjs/codegen.js.map +1 -1
- package/dist/cjs/index.js +21 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/mcp-client.js +159 -0
- package/dist/cjs/mcp-client.js.map +1 -0
- package/dist/cjs/pkce.js +49 -0
- package/dist/cjs/pkce.js.map +1 -0
- package/dist/cjs/registry-consumer.js +217 -2
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/server.js +33 -2
- package/dist/cjs/server.js.map +1 -1
- package/dist/codegen.d.ts +25 -0
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +46 -2
- package/dist/codegen.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-client.d.ts +87 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +152 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/pkce.d.ts +29 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +44 -0
- package/dist/pkce.js.map +1 -0
- package/dist/registry-consumer.d.ts +4 -0
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +216 -2
- package/dist/registry-consumer.js.map +1 -1
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +33 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-definitions/config.ts +318 -0
- package/src/codegen.ts +76 -1
- package/src/index.ts +35 -2
- package/src/mcp-client.ts +230 -0
- package/src/pkce.ts +54 -0
- package/src/registry-consumer.ts +257 -2
- package/src/server.ts +49 -2
package/src/index.ts
CHANGED
|
@@ -211,7 +211,8 @@ export type {
|
|
|
211
211
|
|
|
212
212
|
// Postgres Secret Store
|
|
213
213
|
|
|
214
|
-
// Integrations
|
|
214
|
+
// Integrations (DEPRECATED — use createConfigAgent + refs instead)
|
|
215
|
+
/** @deprecated Use createConfigAgent instead */
|
|
215
216
|
export {
|
|
216
217
|
createIntegrationsAgent,
|
|
217
218
|
createInMemoryIntegrationStore,
|
|
@@ -280,7 +281,11 @@ export type {
|
|
|
280
281
|
ResolvedConfig,
|
|
281
282
|
} from "./define-config.js";
|
|
282
283
|
|
|
283
|
-
export {
|
|
284
|
+
export {
|
|
285
|
+
createRegistryConsumer,
|
|
286
|
+
REGISTRY_TYPE_MCP,
|
|
287
|
+
REGISTRY_TYPE_HTTPS,
|
|
288
|
+
} from "./registry-consumer.js";
|
|
284
289
|
export type {
|
|
285
290
|
RegistryConsumer,
|
|
286
291
|
RegistryConsumerOptions,
|
|
@@ -289,12 +294,32 @@ export type {
|
|
|
289
294
|
SecretResolver,
|
|
290
295
|
} from "./registry-consumer.js";
|
|
291
296
|
|
|
297
|
+
// PKCE
|
|
298
|
+
export {
|
|
299
|
+
generateCodeVerifier,
|
|
300
|
+
generateCodeChallenge,
|
|
301
|
+
generatePkcePair,
|
|
302
|
+
} from "./pkce.js";
|
|
303
|
+
|
|
304
|
+
// MCP Client Auth (OAuth utilities for connecting to MCP servers/registries)
|
|
305
|
+
export {
|
|
306
|
+
discoverOAuthMetadata,
|
|
307
|
+
dynamicClientRegistration,
|
|
308
|
+
buildOAuthAuthorizeUrl,
|
|
309
|
+
exchangeCodeForTokens,
|
|
310
|
+
refreshAccessToken as refreshMcpAccessToken,
|
|
311
|
+
} from "./mcp-client.js";
|
|
312
|
+
export type {
|
|
313
|
+
OAuthServerMetadata,
|
|
314
|
+
} from "./mcp-client.js";
|
|
315
|
+
|
|
292
316
|
// Codegen
|
|
293
317
|
export { codegen, useAgent, listAgentTools } from "./codegen.js";
|
|
294
318
|
export type {
|
|
295
319
|
CodegenOptions,
|
|
296
320
|
CodegenResult,
|
|
297
321
|
CodegenManifest,
|
|
322
|
+
ConnectionSpec,
|
|
298
323
|
McpToolDefinition,
|
|
299
324
|
McpServerInfo,
|
|
300
325
|
McpTransport,
|
|
@@ -375,8 +400,16 @@ export type { ValidationResult } from "./validate.js";
|
|
|
375
400
|
export {
|
|
376
401
|
createBM25Index,
|
|
377
402
|
} from "./bm25.js";
|
|
403
|
+
|
|
378
404
|
export type {
|
|
379
405
|
BM25Options,
|
|
380
406
|
BM25Document,
|
|
381
407
|
BM25Result,
|
|
382
408
|
} from "./bm25.js";
|
|
409
|
+
|
|
410
|
+
// Config Agent
|
|
411
|
+
export { createConfigAgent } from "./agent-definitions/config.js";
|
|
412
|
+
export type {
|
|
413
|
+
ConfigAgentOptions,
|
|
414
|
+
FsStore,
|
|
415
|
+
} from "./agent-definitions/config.js";
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client Auth — OAuth utilities for connecting to MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Standalone utilities for:
|
|
5
|
+
* - OAuth Authorization Server discovery (.well-known/oauth-authorization-server, RFC 8414)
|
|
6
|
+
* - Dynamic client registration (RFC 7591)
|
|
7
|
+
* - PKCE OAuth authorization URL construction
|
|
8
|
+
* - Authorization code → token exchange (with PKCE)
|
|
9
|
+
* - Token refresh
|
|
10
|
+
*
|
|
11
|
+
* These are used by registry-consumer.ts when connecting to MCP servers
|
|
12
|
+
* or registries that require OAuth. The MCP transport itself is handled
|
|
13
|
+
* by registry-consumer — this module only provides auth primitives.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { generatePkcePair } from "./pkce.js";
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
/** OAuth Authorization Server Metadata (RFC 8414) */
|
|
23
|
+
export interface OAuthServerMetadata {
|
|
24
|
+
issuer: string;
|
|
25
|
+
authorization_endpoint: string;
|
|
26
|
+
token_endpoint: string;
|
|
27
|
+
registration_endpoint?: string;
|
|
28
|
+
scopes_supported?: string[];
|
|
29
|
+
response_types_supported?: string[];
|
|
30
|
+
grant_types_supported?: string[];
|
|
31
|
+
code_challenge_methods_supported?: string[];
|
|
32
|
+
token_endpoint_auth_methods_supported?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
36
|
+
// OAuth Discovery
|
|
37
|
+
// ============================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Discover OAuth authorization server metadata.
|
|
41
|
+
* Probes .well-known/oauth-authorization-server (RFC 8414).
|
|
42
|
+
* Returns null if the server doesn't support OAuth.
|
|
43
|
+
*/
|
|
44
|
+
export async function discoverOAuthMetadata(
|
|
45
|
+
serverUrl: string,
|
|
46
|
+
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
47
|
+
): Promise<OAuthServerMetadata | null> {
|
|
48
|
+
const url = `${serverUrl.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetchFn(url);
|
|
51
|
+
if (!res.ok) return null;
|
|
52
|
+
return (await res.json()) as OAuthServerMetadata;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// Dynamic Client Registration
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dynamically register a client with an OAuth server.
|
|
64
|
+
* RFC 7591 — used when the MCP server supports dynamic registration.
|
|
65
|
+
*/
|
|
66
|
+
export async function dynamicClientRegistration(
|
|
67
|
+
registrationEndpoint: string,
|
|
68
|
+
params: {
|
|
69
|
+
clientName: string;
|
|
70
|
+
redirectUris?: string[];
|
|
71
|
+
grantTypes?: string[];
|
|
72
|
+
tokenEndpointAuthMethod?: string;
|
|
73
|
+
},
|
|
74
|
+
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
75
|
+
): Promise<{ clientId: string; clientSecret?: string }> {
|
|
76
|
+
const res = await fetchFn(registrationEndpoint, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
client_name: params.clientName,
|
|
81
|
+
redirect_uris: params.redirectUris,
|
|
82
|
+
grant_types: params.grantTypes ?? ["authorization_code"],
|
|
83
|
+
token_endpoint_auth_method:
|
|
84
|
+
params.tokenEndpointAuthMethod ?? "none",
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const text = await res.text().catch(() => "unknown");
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Dynamic client registration failed: ${res.status} ${text}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
94
|
+
return {
|
|
95
|
+
clientId: data.client_id as string,
|
|
96
|
+
clientSecret: data.client_secret as string | undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================
|
|
101
|
+
// Authorization URL
|
|
102
|
+
// ============================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build an OAuth authorization URL with PKCE.
|
|
106
|
+
* Returns the URL + the code_verifier (to be stored server-side).
|
|
107
|
+
*/
|
|
108
|
+
export async function buildOAuthAuthorizeUrl(params: {
|
|
109
|
+
authorizationEndpoint: string;
|
|
110
|
+
clientId: string;
|
|
111
|
+
redirectUri: string;
|
|
112
|
+
scopes?: string[];
|
|
113
|
+
state?: string;
|
|
114
|
+
}): Promise<{
|
|
115
|
+
url: string;
|
|
116
|
+
codeVerifier: string;
|
|
117
|
+
}> {
|
|
118
|
+
const pkce = await generatePkcePair();
|
|
119
|
+
const url = new URL(params.authorizationEndpoint);
|
|
120
|
+
url.searchParams.set("response_type", "code");
|
|
121
|
+
url.searchParams.set("client_id", params.clientId);
|
|
122
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
123
|
+
url.searchParams.set("code_challenge", pkce.codeChallenge);
|
|
124
|
+
url.searchParams.set("code_challenge_method", pkce.codeChallengeMethod);
|
|
125
|
+
if (params.scopes?.length) {
|
|
126
|
+
url.searchParams.set("scope", params.scopes.join(" "));
|
|
127
|
+
}
|
|
128
|
+
if (params.state) {
|
|
129
|
+
url.searchParams.set("state", params.state);
|
|
130
|
+
}
|
|
131
|
+
return { url: url.toString(), codeVerifier: pkce.codeVerifier };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================
|
|
135
|
+
// Token Exchange
|
|
136
|
+
// ============================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Exchange an authorization code for tokens (with PKCE).
|
|
140
|
+
*/
|
|
141
|
+
export async function exchangeCodeForTokens(
|
|
142
|
+
tokenEndpoint: string,
|
|
143
|
+
params: {
|
|
144
|
+
code: string;
|
|
145
|
+
codeVerifier: string;
|
|
146
|
+
clientId: string;
|
|
147
|
+
clientSecret?: string;
|
|
148
|
+
redirectUri: string;
|
|
149
|
+
},
|
|
150
|
+
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
151
|
+
): Promise<{
|
|
152
|
+
accessToken: string;
|
|
153
|
+
refreshToken?: string;
|
|
154
|
+
expiresIn?: number;
|
|
155
|
+
tokenType?: string;
|
|
156
|
+
}> {
|
|
157
|
+
const body = new URLSearchParams({
|
|
158
|
+
grant_type: "authorization_code",
|
|
159
|
+
code: params.code,
|
|
160
|
+
code_verifier: params.codeVerifier,
|
|
161
|
+
client_id: params.clientId,
|
|
162
|
+
redirect_uri: params.redirectUri,
|
|
163
|
+
});
|
|
164
|
+
if (params.clientSecret) {
|
|
165
|
+
body.set("client_secret", params.clientSecret);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const res = await fetchFn(tokenEndpoint, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
171
|
+
body: body.toString(),
|
|
172
|
+
});
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
const text = await res.text().catch(() => "unknown");
|
|
175
|
+
throw new Error(`Token exchange failed: ${res.status} ${text}`);
|
|
176
|
+
}
|
|
177
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
178
|
+
return {
|
|
179
|
+
accessToken: data.access_token as string,
|
|
180
|
+
refreshToken: data.refresh_token as string | undefined,
|
|
181
|
+
expiresIn: data.expires_in as number | undefined,
|
|
182
|
+
tokenType: data.token_type as string | undefined,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================
|
|
187
|
+
// Token Refresh
|
|
188
|
+
// ============================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Refresh an access token.
|
|
192
|
+
*/
|
|
193
|
+
export async function refreshAccessToken(
|
|
194
|
+
tokenEndpoint: string,
|
|
195
|
+
params: {
|
|
196
|
+
refreshToken: string;
|
|
197
|
+
clientId: string;
|
|
198
|
+
clientSecret?: string;
|
|
199
|
+
},
|
|
200
|
+
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
201
|
+
): Promise<{
|
|
202
|
+
accessToken: string;
|
|
203
|
+
refreshToken?: string;
|
|
204
|
+
expiresIn?: number;
|
|
205
|
+
}> {
|
|
206
|
+
const body = new URLSearchParams({
|
|
207
|
+
grant_type: "refresh_token",
|
|
208
|
+
refresh_token: params.refreshToken,
|
|
209
|
+
client_id: params.clientId,
|
|
210
|
+
});
|
|
211
|
+
if (params.clientSecret) {
|
|
212
|
+
body.set("client_secret", params.clientSecret);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const res = await fetchFn(tokenEndpoint, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
218
|
+
body: body.toString(),
|
|
219
|
+
});
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
const text = await res.text().catch(() => "unknown");
|
|
222
|
+
throw new Error(`Token refresh failed: ${res.status} ${text}`);
|
|
223
|
+
}
|
|
224
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
225
|
+
return {
|
|
226
|
+
accessToken: data.access_token as string,
|
|
227
|
+
refreshToken: data.refresh_token as string | undefined,
|
|
228
|
+
expiresIn: data.expires_in as number | undefined,
|
|
229
|
+
};
|
|
230
|
+
}
|
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
|
+
}
|
package/src/registry-consumer.ts
CHANGED
|
@@ -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
|
-
|
|
519
|
+
// Collect from standard registries
|
|
520
|
+
const registryResults = await Promise.allSettled(
|
|
324
521
|
resolvedRegistries.map(listFromRegistry),
|
|
325
522
|
);
|
|
326
|
-
|
|
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
|
);
|