@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.
- 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 +27 -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 +4 -0
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +27 -2
- package/dist/codegen.js.map +1 -1
- package/dist/index.d.ts +7 -1
- 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 +33 -1
- package/src/index.ts +34 -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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Agent (@config)
|
|
3
|
+
*
|
|
4
|
+
* Built-in agent for managing consumer configuration — refs and registries.
|
|
5
|
+
* Replaces @integrations and the LLM-facing parts of @auth.
|
|
6
|
+
*
|
|
7
|
+
* Provides:
|
|
8
|
+
* - add_ref / remove_ref / list_refs for managing agent refs
|
|
9
|
+
* - add_registry for registering new registries
|
|
10
|
+
* - FsStore interface for pluggable filesystem storage (VCS-backed or local)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { createAgentRegistry, createConfigAgent } from '@slashfi/agents-sdk';
|
|
15
|
+
*
|
|
16
|
+
* const registry = createAgentRegistry();
|
|
17
|
+
* registry.register(createConfigAgent({
|
|
18
|
+
* store: myFsStore,
|
|
19
|
+
* }));
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ConsumerConfig, RefEntry } from "../define-config.js";
|
|
24
|
+
import { normalizeRef } from "../define-config.js";
|
|
25
|
+
import { defineAgent, defineTool } from "../define.js";
|
|
26
|
+
import type { AgentDefinition, ToolContext } from "../types.js";
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// FsStore Interface
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Filesystem store for reading/writing consumer configs.
|
|
34
|
+
* The storage engine (VCS, local fs, etc.) is abstracted away.
|
|
35
|
+
*/
|
|
36
|
+
export interface FsStore {
|
|
37
|
+
/** Read a file as UTF-8 string. Returns null if not found. */
|
|
38
|
+
readFile(path: string): Promise<string | null>;
|
|
39
|
+
/** Write a file with UTF-8 content. Creates parent dirs if needed. */
|
|
40
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================
|
|
44
|
+
// Config Persistence
|
|
45
|
+
// ============================================
|
|
46
|
+
|
|
47
|
+
const CONFIG_PATH = "consumer-config.json";
|
|
48
|
+
|
|
49
|
+
async function readConfig(store: FsStore): Promise<ConsumerConfig> {
|
|
50
|
+
const content = await store.readFile(CONFIG_PATH);
|
|
51
|
+
if (!content) return {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(content) as ConsumerConfig;
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function writeConfig(
|
|
60
|
+
store: FsStore,
|
|
61
|
+
config: ConsumerConfig,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
await store.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================
|
|
67
|
+
// Config Agent Options
|
|
68
|
+
// ============================================
|
|
69
|
+
|
|
70
|
+
export interface ConfigAgentOptions {
|
|
71
|
+
/** Filesystem store for persisting consumer config */
|
|
72
|
+
store: FsStore;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the FsStore for a specific user.
|
|
76
|
+
* When provided, refs are stored per-user.
|
|
77
|
+
* The callerId from ToolContext is passed.
|
|
78
|
+
*/
|
|
79
|
+
resolveUserStore?: (callerId: string) => FsStore;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// Create Config Agent
|
|
84
|
+
// ============================================
|
|
85
|
+
|
|
86
|
+
export function createConfigAgent(
|
|
87
|
+
options: ConfigAgentOptions,
|
|
88
|
+
): AgentDefinition {
|
|
89
|
+
const { store, resolveUserStore } = options;
|
|
90
|
+
|
|
91
|
+
function getStore(ctx: ToolContext): FsStore {
|
|
92
|
+
if (resolveUserStore && ctx.callerId) {
|
|
93
|
+
return resolveUserStore(ctx.callerId);
|
|
94
|
+
}
|
|
95
|
+
return store;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- add_ref ----
|
|
99
|
+
const addRefTool = defineTool({
|
|
100
|
+
name: "add_ref",
|
|
101
|
+
description:
|
|
102
|
+
"Add or update an agent ref in the consumer config. " +
|
|
103
|
+
"The ref is persisted to the config store.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object" as const,
|
|
106
|
+
properties: {
|
|
107
|
+
ref: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: 'Agent ref name (e.g. "notion", "linear")',
|
|
110
|
+
},
|
|
111
|
+
as: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "Local alias (for multi-instance refs)",
|
|
114
|
+
},
|
|
115
|
+
url: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "Direct URL to the agent",
|
|
118
|
+
},
|
|
119
|
+
config: {
|
|
120
|
+
type: "object",
|
|
121
|
+
additionalProperties: { type: "string" },
|
|
122
|
+
description: "Per-instance config (secret URIs or literal values)",
|
|
123
|
+
},
|
|
124
|
+
registry: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description:
|
|
127
|
+
'Registry to resolve from (e.g. "slash", "mcp", "https")',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ["ref"],
|
|
131
|
+
},
|
|
132
|
+
execute: async (
|
|
133
|
+
input: {
|
|
134
|
+
ref: string;
|
|
135
|
+
as?: string;
|
|
136
|
+
url?: string;
|
|
137
|
+
config?: Record<string, string>;
|
|
138
|
+
registry?: string;
|
|
139
|
+
},
|
|
140
|
+
ctx: ToolContext,
|
|
141
|
+
) => {
|
|
142
|
+
const fs = getStore(ctx);
|
|
143
|
+
const currentConfig = await readConfig(fs);
|
|
144
|
+
|
|
145
|
+
const entry: RefEntry = {
|
|
146
|
+
ref: input.ref,
|
|
147
|
+
...(input.as && { as: input.as }),
|
|
148
|
+
...(input.url && { url: input.url }),
|
|
149
|
+
...(input.config && { config: input.config }),
|
|
150
|
+
...(input.registry && { registry: input.registry }),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Upsert: find existing ref by name/alias, replace or append
|
|
154
|
+
const name = input.as ?? input.ref;
|
|
155
|
+
const refs = currentConfig.refs ?? [];
|
|
156
|
+
const existingIdx = refs.findIndex((r) => {
|
|
157
|
+
const normalized = normalizeRef(r);
|
|
158
|
+
return normalized.name === name;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (existingIdx >= 0) {
|
|
162
|
+
refs[existingIdx] = entry;
|
|
163
|
+
} else {
|
|
164
|
+
refs.push(entry);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
currentConfig.refs = refs;
|
|
168
|
+
await writeConfig(fs, currentConfig);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
added: true,
|
|
172
|
+
ref: input.ref,
|
|
173
|
+
name,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ---- remove_ref ----
|
|
179
|
+
const removeRefTool = defineTool({
|
|
180
|
+
name: "remove_ref",
|
|
181
|
+
description: "Remove an agent ref from the consumer config by name or alias.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object" as const,
|
|
184
|
+
properties: {
|
|
185
|
+
name: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "Ref name or alias to remove",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["name"],
|
|
191
|
+
},
|
|
192
|
+
execute: async (
|
|
193
|
+
input: { name: string },
|
|
194
|
+
ctx: ToolContext,
|
|
195
|
+
) => {
|
|
196
|
+
const fs = getStore(ctx);
|
|
197
|
+
const currentConfig = await readConfig(fs);
|
|
198
|
+
const refs = currentConfig.refs ?? [];
|
|
199
|
+
|
|
200
|
+
const before = refs.length;
|
|
201
|
+
currentConfig.refs = refs.filter((r) => {
|
|
202
|
+
const normalized = normalizeRef(r);
|
|
203
|
+
return normalized.name !== input.name;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (currentConfig.refs.length === before) {
|
|
207
|
+
return { removed: false, error: `Ref "${input.name}" not found` };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await writeConfig(fs, currentConfig);
|
|
211
|
+
return { removed: true, name: input.name };
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ---- list_refs ----
|
|
216
|
+
const listRefsTool = defineTool({
|
|
217
|
+
name: "list_refs",
|
|
218
|
+
description:
|
|
219
|
+
"List all agent refs in the consumer config. " +
|
|
220
|
+
"Returns normalized refs with their names and config.",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object" as const,
|
|
223
|
+
properties: {},
|
|
224
|
+
},
|
|
225
|
+
execute: async (_input: unknown, ctx: ToolContext) => {
|
|
226
|
+
const fs = getStore(ctx);
|
|
227
|
+
const currentConfig = await readConfig(fs);
|
|
228
|
+
const refs = (currentConfig.refs ?? []).map(normalizeRef);
|
|
229
|
+
return { refs };
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ---- add_registry ----
|
|
234
|
+
const addRegistryTool = defineTool({
|
|
235
|
+
name: "add_registry",
|
|
236
|
+
description:
|
|
237
|
+
"Add or update a registry in the consumer config. " +
|
|
238
|
+
"Registries are where refs resolve from.",
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object" as const,
|
|
241
|
+
properties: {
|
|
242
|
+
name: {
|
|
243
|
+
type: "string",
|
|
244
|
+
description: 'Human-readable name (e.g. "slash", "internal")',
|
|
245
|
+
},
|
|
246
|
+
url: {
|
|
247
|
+
type: "string",
|
|
248
|
+
description: "Registry URL",
|
|
249
|
+
},
|
|
250
|
+
auth: {
|
|
251
|
+
type: "object",
|
|
252
|
+
description: "Auth config for the registry",
|
|
253
|
+
properties: {
|
|
254
|
+
type: {
|
|
255
|
+
type: "string",
|
|
256
|
+
enum: ["none", "bearer", "api-key", "jwt"],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
required: ["url"],
|
|
262
|
+
},
|
|
263
|
+
execute: async (
|
|
264
|
+
input: {
|
|
265
|
+
name?: string;
|
|
266
|
+
url: string;
|
|
267
|
+
auth?: { type: string; [key: string]: unknown };
|
|
268
|
+
},
|
|
269
|
+
ctx: ToolContext,
|
|
270
|
+
) => {
|
|
271
|
+
const fs = getStore(ctx);
|
|
272
|
+
const currentConfig = await readConfig(fs);
|
|
273
|
+
|
|
274
|
+
const registries = currentConfig.registries ?? [];
|
|
275
|
+
const entry = {
|
|
276
|
+
url: input.url,
|
|
277
|
+
...(input.name && { name: input.name }),
|
|
278
|
+
...(input.auth && { auth: input.auth as any }),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Upsert by URL
|
|
282
|
+
const existingIdx = registries.findIndex((r) => {
|
|
283
|
+
const url = typeof r === "string" ? r : r.url;
|
|
284
|
+
return url === input.url;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (existingIdx >= 0) {
|
|
288
|
+
registries[existingIdx] = entry;
|
|
289
|
+
} else {
|
|
290
|
+
registries.push(entry);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
currentConfig.registries = registries;
|
|
294
|
+
await writeConfig(fs, currentConfig);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
added: true,
|
|
298
|
+
url: input.url,
|
|
299
|
+
name: input.name ?? new URL(input.url).hostname,
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---- Define the agent ----
|
|
305
|
+
return defineAgent({
|
|
306
|
+
path: "@config",
|
|
307
|
+
entrypoint:
|
|
308
|
+
"Consumer config management. Use add_ref/remove_ref/list_refs to manage agent refs, " +
|
|
309
|
+
"and add_registry to configure registries.",
|
|
310
|
+
config: {
|
|
311
|
+
name: "Config",
|
|
312
|
+
description:
|
|
313
|
+
"Manage consumer config — add/remove/list agent refs and registries. " +
|
|
314
|
+
"Replaces @integrations for connecting to third-party services.",
|
|
315
|
+
},
|
|
316
|
+
tools: [addRefTool, removeRefTool, listRefsTool, addRegistryTool] as any,
|
|
317
|
+
});
|
|
318
|
+
}
|
package/src/codegen.ts
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
32
32
|
import { join, resolve } from "node:path";
|
|
33
33
|
import type { JsonSchema } from "./types.js";
|
|
34
|
+
import { discoverOAuthMetadata, type OAuthServerMetadata } from "./mcp-client.js";
|
|
34
35
|
|
|
35
36
|
// ============================================
|
|
36
37
|
// Types
|
|
@@ -108,6 +109,9 @@ export interface CodegenResult {
|
|
|
108
109
|
|
|
109
110
|
/** All generated file paths */
|
|
110
111
|
files: string[];
|
|
112
|
+
|
|
113
|
+
/** OAuth server metadata (if discovered via .well-known/oauth-authorization-server) */
|
|
114
|
+
oauth?: OAuthServerMetadata;
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
// ============================================
|
|
@@ -614,6 +618,23 @@ function createSseTransport(source: {
|
|
|
614
618
|
// Source Parsing
|
|
615
619
|
// ============================================
|
|
616
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Extract the base URL from a server source (for OAuth discovery).
|
|
623
|
+
* Returns null for stdio/command-based sources.
|
|
624
|
+
*/
|
|
625
|
+
function resolveServerUrl(source: ServerSource): string | null {
|
|
626
|
+
if (typeof source === "string") {
|
|
627
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
628
|
+
return source.replace(/\/sse$/, "");
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
if ("url" in source) {
|
|
633
|
+
return source.url.replace(/\/sse$/, "");
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
617
638
|
function parseServerSource(source: ServerSource): McpTransport {
|
|
618
639
|
if (typeof source === "string") {
|
|
619
640
|
// URL -> HTTP or SSE transport
|
|
@@ -1113,6 +1134,7 @@ export interface CodegenManifest {
|
|
|
1113
1134
|
serverSource: ServerSource;
|
|
1114
1135
|
serverInfo: McpServerInfo;
|
|
1115
1136
|
tools: { name: string; description?: string }[];
|
|
1137
|
+
oauth?: OAuthServerMetadata;
|
|
1116
1138
|
generatedAt: string;
|
|
1117
1139
|
}
|
|
1118
1140
|
|
|
@@ -1121,12 +1143,14 @@ function generateManifest(
|
|
|
1121
1143
|
serverInfo: McpServerInfo,
|
|
1122
1144
|
tools: McpToolDefinition[],
|
|
1123
1145
|
agentPath: string,
|
|
1146
|
+
oauth?: OAuthServerMetadata | null,
|
|
1124
1147
|
): string {
|
|
1125
1148
|
const manifest: CodegenManifest = {
|
|
1126
1149
|
agentPath,
|
|
1127
1150
|
serverSource,
|
|
1128
1151
|
serverInfo,
|
|
1129
1152
|
tools: tools.map((t) => ({ name: t.name, description: t.description })),
|
|
1153
|
+
...(oauth ? { oauth } : {}),
|
|
1130
1154
|
generatedAt: new Date().toISOString(),
|
|
1131
1155
|
};
|
|
1132
1156
|
return JSON.stringify(manifest, null, 2) + "\n";
|
|
@@ -1204,6 +1228,13 @@ export async function codegen(options: CodegenOptions): Promise<CodegenResult> {
|
|
|
1204
1228
|
);
|
|
1205
1229
|
}
|
|
1206
1230
|
|
|
1231
|
+
// 3.5. Discover OAuth metadata (for URL-based servers)
|
|
1232
|
+
let oauth: OAuthServerMetadata | null = null;
|
|
1233
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
1234
|
+
if (serverUrl) {
|
|
1235
|
+
oauth = await discoverOAuthMetadata(serverUrl);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1207
1238
|
// 4. Derive agent path
|
|
1208
1239
|
const agentPath =
|
|
1209
1240
|
options.agentPath ??
|
|
@@ -1253,7 +1284,7 @@ export async function codegen(options: CodegenOptions): Promise<CodegenResult> {
|
|
|
1253
1284
|
}
|
|
1254
1285
|
|
|
1255
1286
|
// 11. Generate manifest (for `agents-sdk use`)
|
|
1256
|
-
const manifest = generateManifest(options.server, serverInfo, tools, agentPath);
|
|
1287
|
+
const manifest = generateManifest(options.server, serverInfo, tools, agentPath, oauth);
|
|
1257
1288
|
writeFileSync(join(outDir, ".codegen-manifest.json"), manifest);
|
|
1258
1289
|
files.push(".codegen-manifest.json");
|
|
1259
1290
|
|
|
@@ -1263,6 +1294,7 @@ export async function codegen(options: CodegenOptions): Promise<CodegenResult> {
|
|
|
1263
1294
|
toolCount: tools.length,
|
|
1264
1295
|
toolFiles,
|
|
1265
1296
|
files,
|
|
1297
|
+
...(oauth ? { oauth } : {}),
|
|
1266
1298
|
};
|
|
1267
1299
|
}
|
|
1268
1300
|
|
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,6 +294,25 @@ 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 {
|
|
@@ -375,8 +399,16 @@ export type { ValidationResult } from "./validate.js";
|
|
|
375
399
|
export {
|
|
376
400
|
createBM25Index,
|
|
377
401
|
} from "./bm25.js";
|
|
402
|
+
|
|
378
403
|
export type {
|
|
379
404
|
BM25Options,
|
|
380
405
|
BM25Document,
|
|
381
406
|
BM25Result,
|
|
382
407
|
} from "./bm25.js";
|
|
408
|
+
|
|
409
|
+
// Config Agent
|
|
410
|
+
export { createConfigAgent } from "./agent-definitions/config.js";
|
|
411
|
+
export type {
|
|
412
|
+
ConfigAgentOptions,
|
|
413
|
+
FsStore,
|
|
414
|
+
} 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
|
+
}
|