@slashfi/agents-sdk 0.1.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/src/server.ts ADDED
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Agent Server
3
+ *
4
+ * HTTP server that exposes the agent registry via JSON-RPC endpoints.
5
+ * Compatible with MCP (Model Context Protocol) over HTTP.
6
+ *
7
+ * Endpoints:
8
+ * - POST /call - Execute agent actions (execute_tool, describe_tools, load)
9
+ * - GET /list - List registered agents
10
+ * - POST /oauth/token - OAuth2 token endpoint (when @auth is registered)
11
+ *
12
+ * Auth Integration:
13
+ * When an `@auth` agent is registered, the server automatically:
14
+ * - Validates Bearer tokens on requests
15
+ * - Resolves tokens to identity + scopes
16
+ * - Populates callerId, callerType in the request context
17
+ * - Recognizes the root key for admin access
18
+ * - Mounts the /oauth/token endpoint
19
+ */
20
+
21
+ import type { AuthStore } from "./auth.js";
22
+ import type { AgentRegistry } from "./registry.js";
23
+ import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
24
+
25
+ // ============================================
26
+ // Server Types
27
+ // ============================================
28
+
29
+ /**
30
+ * Server configuration options.
31
+ */
32
+ export interface AgentServerOptions {
33
+ /** Port to listen on (default: 3000) */
34
+ port?: number;
35
+
36
+ /** Hostname to bind to (default: 'localhost') */
37
+ hostname?: string;
38
+
39
+ /** Base path for endpoints (default: '') */
40
+ basePath?: string;
41
+
42
+ /** Enable CORS (default: true) */
43
+ cors?: boolean;
44
+
45
+ /** Custom request handler for unmatched routes */
46
+ onNotFound?: (req: Request) => Response | Promise<Response>;
47
+ }
48
+
49
+ /**
50
+ * Agent server instance.
51
+ */
52
+ export interface AgentServer {
53
+ /** Start the server */
54
+ start(): Promise<void>;
55
+
56
+ /** Stop the server */
57
+ stop(): Promise<void>;
58
+
59
+ /** Handle a request (for custom integrations) */
60
+ fetch(req: Request): Promise<Response>;
61
+
62
+ /** Get the server URL (only available after start) */
63
+ url: string | null;
64
+ }
65
+
66
+ // ============================================
67
+ // Auth Integration Types
68
+ // ============================================
69
+
70
+ interface AuthConfig {
71
+ store: AuthStore;
72
+ rootKey: string;
73
+ tokenTtl: number;
74
+ }
75
+
76
+ interface ResolvedAuth {
77
+ callerId: string;
78
+ callerType: "agent" | "user" | "system";
79
+ scopes: string[];
80
+ isRoot: boolean;
81
+ }
82
+
83
+ // ============================================
84
+ // Response Helpers
85
+ // ============================================
86
+
87
+ function jsonResponse(data: unknown, status = 200): Response {
88
+ return new Response(JSON.stringify(data), {
89
+ status,
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ },
93
+ });
94
+ }
95
+
96
+ function corsHeaders(): Record<string, string> {
97
+ return {
98
+ "Access-Control-Allow-Origin": "*",
99
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
100
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
101
+ };
102
+ }
103
+
104
+ // ============================================
105
+ // Auth Detection
106
+ // ============================================
107
+
108
+ function detectAuth(registry: AgentRegistry): AuthConfig | null {
109
+ const authAgent = registry.get("@auth") as
110
+ | (AgentDefinition & {
111
+ __authStore?: AuthStore;
112
+ __rootKey?: string;
113
+ __tokenTtl?: number;
114
+ })
115
+ | undefined;
116
+
117
+ if (!authAgent?.__authStore || !authAgent.__rootKey) return null;
118
+
119
+ return {
120
+ store: authAgent.__authStore,
121
+ rootKey: authAgent.__rootKey,
122
+ tokenTtl: authAgent.__tokenTtl ?? 3600,
123
+ };
124
+ }
125
+
126
+ async function resolveAuth(
127
+ req: Request,
128
+ authConfig: AuthConfig,
129
+ ): Promise<ResolvedAuth | null> {
130
+ const authHeader = req.headers.get("Authorization");
131
+ if (!authHeader) return null;
132
+
133
+ const [scheme, credential] = authHeader.split(" ", 2);
134
+ if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
135
+
136
+ // Check root key
137
+ if (credential === authConfig.rootKey) {
138
+ return {
139
+ callerId: "root",
140
+ callerType: "system",
141
+ scopes: ["*"],
142
+ isRoot: true,
143
+ };
144
+ }
145
+
146
+ // Validate token
147
+ const token = await authConfig.store.validateToken(credential);
148
+ if (!token) return null;
149
+
150
+ // Look up client name
151
+ const client = await authConfig.store.getClient(token.clientId);
152
+
153
+ return {
154
+ callerId: client?.name ?? token.clientId,
155
+ callerType: "agent",
156
+ scopes: token.scopes,
157
+ isRoot: false,
158
+ };
159
+ }
160
+
161
+ // ============================================
162
+ // Visibility Filtering for /list
163
+ // ============================================
164
+
165
+ function canSeeAgent(
166
+ agent: AgentDefinition,
167
+ auth: ResolvedAuth | null,
168
+ ): boolean {
169
+ const visibility: Visibility = agent.visibility ?? "internal";
170
+
171
+ if (auth?.isRoot) return true;
172
+ if (visibility === "public") return true;
173
+ if (visibility === "internal" && auth) return true;
174
+ return false;
175
+ }
176
+
177
+ // ============================================
178
+ // Create Server
179
+ // ============================================
180
+
181
+ /**
182
+ * Create an HTTP server for the agent registry.
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * const registry = createAgentRegistry();
187
+ * registry.register(createAuthAgent({ rootKey: 'rk_xxx' }));
188
+ * registry.register(myAgent);
189
+ *
190
+ * const server = createAgentServer(registry, { port: 3000 });
191
+ * await server.start();
192
+ * // POST /call - Execute agent actions
193
+ * // GET /list - List agents (filtered by auth)
194
+ * // POST /oauth/token - OAuth2 token endpoint
195
+ * ```
196
+ */
197
+ export function createAgentServer(
198
+ registry: AgentRegistry,
199
+ options: AgentServerOptions = {},
200
+ ): AgentServer {
201
+ const {
202
+ port = 3000,
203
+ hostname = "localhost",
204
+ basePath = "",
205
+ cors = true,
206
+ onNotFound,
207
+ } = options;
208
+
209
+ let serverInstance: ReturnType<typeof Bun.serve> | null = null;
210
+ let serverUrl: string | null = null;
211
+
212
+ // Detect auth configuration
213
+ const authConfig = detectAuth(registry);
214
+
215
+ /**
216
+ * Handle incoming requests.
217
+ */
218
+ async function fetch(req: Request): Promise<Response> {
219
+ const url = new URL(req.url);
220
+ const path = url.pathname.replace(basePath, "") || "/";
221
+
222
+ // Handle CORS preflight
223
+ if (cors && req.method === "OPTIONS") {
224
+ return new Response(null, {
225
+ status: 204,
226
+ headers: corsHeaders(),
227
+ });
228
+ }
229
+
230
+ // Add CORS headers to response
231
+ const addCors = (response: Response): Response => {
232
+ if (!cors) return response;
233
+ const headers = new Headers(response.headers);
234
+ for (const [key, value] of Object.entries(corsHeaders())) {
235
+ headers.set(key, value);
236
+ }
237
+ return new Response(response.body, {
238
+ status: response.status,
239
+ statusText: response.statusText,
240
+ headers,
241
+ });
242
+ };
243
+
244
+ // Resolve auth on every request
245
+ const auth = authConfig ? await resolveAuth(req, authConfig) : null;
246
+
247
+ try {
248
+ // POST /oauth/token - Standard OAuth2 endpoint
249
+ if (path === "/oauth/token" && req.method === "POST" && authConfig) {
250
+ const contentType = req.headers.get("Content-Type") ?? "";
251
+ let grantType: string;
252
+ let clientId: string;
253
+ let clientSecret: string;
254
+
255
+ if (contentType.includes("application/x-www-form-urlencoded")) {
256
+ const body = await req.text();
257
+ const params = new URLSearchParams(body);
258
+ grantType = params.get("grant_type") ?? "";
259
+ clientId = params.get("client_id") ?? "";
260
+ clientSecret = params.get("client_secret") ?? "";
261
+ } else {
262
+ const body = (await req.json()) as Record<string, string>;
263
+ grantType = body.grant_type ?? "";
264
+ clientId = body.client_id ?? "";
265
+ clientSecret = body.client_secret ?? "";
266
+ }
267
+
268
+ if (grantType !== "client_credentials") {
269
+ return addCors(
270
+ jsonResponse(
271
+ {
272
+ error: "unsupported_grant_type",
273
+ error_description: "Only client_credentials is supported",
274
+ },
275
+ 400,
276
+ ),
277
+ );
278
+ }
279
+
280
+ if (!clientId || !clientSecret) {
281
+ return addCors(
282
+ jsonResponse(
283
+ {
284
+ error: "invalid_request",
285
+ error_description: "Missing client_id or client_secret",
286
+ },
287
+ 400,
288
+ ),
289
+ );
290
+ }
291
+
292
+ const client = await authConfig.store.validateClient(
293
+ clientId,
294
+ clientSecret,
295
+ );
296
+ if (!client) {
297
+ return addCors(
298
+ jsonResponse(
299
+ {
300
+ error: "invalid_client",
301
+ error_description: "Invalid client credentials",
302
+ },
303
+ 401,
304
+ ),
305
+ );
306
+ }
307
+
308
+ // Generate token
309
+ const tokenString = `at_${Array.from({ length: 48 }, () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 62)]).join("")}`;
310
+ const token = {
311
+ token: tokenString,
312
+ clientId: client.clientId,
313
+ scopes: client.scopes,
314
+ issuedAt: Date.now(),
315
+ expiresAt: Date.now() + authConfig.tokenTtl * 1000,
316
+ };
317
+ await authConfig.store.storeToken(token);
318
+
319
+ // Standard OAuth2 response
320
+ return addCors(
321
+ jsonResponse({
322
+ access_token: token.token,
323
+ token_type: "bearer",
324
+ expires_in: authConfig.tokenTtl,
325
+ scope: client.scopes.join(" "),
326
+ }),
327
+ );
328
+ }
329
+
330
+ // POST /call - Execute agent action
331
+ if (path === "/call" && req.method === "POST") {
332
+ const body = (await req.json()) as CallAgentRequest;
333
+
334
+ if (!body.path || !body.action) {
335
+ return addCors(
336
+ jsonResponse(
337
+ {
338
+ success: false,
339
+ error: "Missing required fields: path, action",
340
+ code: "INVALID_REQUEST",
341
+ },
342
+ 400,
343
+ ),
344
+ );
345
+ }
346
+
347
+ // Inject auth context into request
348
+ if (auth) {
349
+ body.callerId = auth.callerId;
350
+ body.callerType = auth.callerType;
351
+ if (!body.metadata) body.metadata = {};
352
+ body.metadata.scopes = auth.scopes;
353
+ body.metadata.isRoot = auth.isRoot;
354
+ }
355
+
356
+ // Root key bypasses all access checks
357
+ if (auth?.isRoot) {
358
+ body.callerType = "system";
359
+ }
360
+
361
+ const result = await registry.call(body);
362
+ const status = "success" in result && result.success ? 200 : 400;
363
+ return addCors(jsonResponse(result, status));
364
+ }
365
+
366
+ // GET /list - List agents (filtered by visibility)
367
+ if (path === "/list" && req.method === "GET") {
368
+ const agents = registry.list();
369
+ const visible = agents.filter((agent) => canSeeAgent(agent, auth));
370
+
371
+ return addCors(
372
+ jsonResponse({
373
+ success: true,
374
+ agents: visible.map((agent) => ({
375
+ path: agent.path,
376
+ name: agent.config?.name,
377
+ description: agent.config?.description,
378
+ supportedActions: agent.config?.supportedActions,
379
+ tools: agent.tools
380
+ .filter((t) => {
381
+ const tv = t.visibility ?? "internal";
382
+ if (auth?.isRoot) return true;
383
+ if (tv === "public") return true;
384
+ if (tv === "internal" && auth) return true;
385
+ return false;
386
+ })
387
+ .map((t) => t.name),
388
+ })),
389
+ }),
390
+ );
391
+ }
392
+
393
+ // Not found
394
+ if (onNotFound) {
395
+ return addCors(await onNotFound(req));
396
+ }
397
+
398
+ return addCors(
399
+ jsonResponse(
400
+ {
401
+ success: false,
402
+ error: `Not found: ${req.method} ${path}`,
403
+ code: "NOT_FOUND",
404
+ },
405
+ 404,
406
+ ),
407
+ );
408
+ } catch (err) {
409
+ return addCors(
410
+ jsonResponse(
411
+ {
412
+ success: false,
413
+ error: err instanceof Error ? err.message : String(err),
414
+ code: "INTERNAL_ERROR",
415
+ },
416
+ 500,
417
+ ),
418
+ );
419
+ }
420
+ }
421
+
422
+ const server: AgentServer = {
423
+ async start(): Promise<void> {
424
+ if (serverInstance) {
425
+ throw new Error("Server is already running");
426
+ }
427
+
428
+ serverInstance = Bun.serve({
429
+ port,
430
+ hostname,
431
+ fetch,
432
+ });
433
+
434
+ serverUrl = `http://${hostname}:${port}${basePath}`;
435
+ console.log(`Agent server running at ${serverUrl}`);
436
+ console.log(` POST ${basePath}/call - Execute agent actions`);
437
+ console.log(` GET ${basePath}/list - List agents`);
438
+ if (authConfig) {
439
+ console.log(` POST ${basePath}/oauth/token - OAuth2 token endpoint`);
440
+ console.log(" Auth: enabled (root key configured)");
441
+ }
442
+ },
443
+
444
+ async stop(): Promise<void> {
445
+ if (serverInstance) {
446
+ serverInstance.stop();
447
+ serverInstance = null;
448
+ serverUrl = null;
449
+ }
450
+ },
451
+
452
+ fetch,
453
+
454
+ get url(): string | null {
455
+ return serverUrl;
456
+ },
457
+ };
458
+
459
+ return server;
460
+ }