@slashfi/agents-sdk 0.7.0 → 0.9.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 (64) hide show
  1. package/dist/agent-definitions/auth.d.ts +17 -0
  2. package/dist/agent-definitions/auth.d.ts.map +1 -1
  3. package/dist/agent-definitions/auth.js +135 -1
  4. package/dist/agent-definitions/auth.js.map +1 -1
  5. package/dist/agent-definitions/integrations.d.ts +28 -12
  6. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  7. package/dist/agent-definitions/integrations.js +239 -41
  8. package/dist/agent-definitions/integrations.js.map +1 -1
  9. package/dist/agent-definitions/remote-registry.d.ts +32 -0
  10. package/dist/agent-definitions/remote-registry.d.ts.map +1 -0
  11. package/dist/agent-definitions/remote-registry.js +460 -0
  12. package/dist/agent-definitions/remote-registry.js.map +1 -0
  13. package/dist/index.d.ts +12 -5
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +8 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/integration-interface.d.ts +37 -0
  18. package/dist/integration-interface.d.ts.map +1 -0
  19. package/dist/integration-interface.js +94 -0
  20. package/dist/integration-interface.js.map +1 -0
  21. package/dist/integrations-store.d.ts +33 -0
  22. package/dist/integrations-store.d.ts.map +1 -0
  23. package/dist/integrations-store.js +50 -0
  24. package/dist/integrations-store.js.map +1 -0
  25. package/dist/jwt.d.ts +86 -17
  26. package/dist/jwt.d.ts.map +1 -1
  27. package/dist/jwt.js +140 -17
  28. package/dist/jwt.js.map +1 -1
  29. package/dist/registry.d.ts +7 -0
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js +14 -21
  32. package/dist/registry.js.map +1 -1
  33. package/dist/secret-collection.d.ts +37 -0
  34. package/dist/secret-collection.d.ts.map +1 -0
  35. package/dist/secret-collection.js +37 -0
  36. package/dist/secret-collection.js.map +1 -0
  37. package/dist/server.d.ts +41 -42
  38. package/dist/server.d.ts.map +1 -1
  39. package/dist/server.js +232 -555
  40. package/dist/server.js.map +1 -1
  41. package/dist/types.d.ts +24 -2
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +5 -2
  44. package/src/agent-definitions/auth.ts +187 -1
  45. package/src/agent-definitions/integrations.ts +287 -55
  46. package/src/agent-definitions/remote-registry.ts +621 -0
  47. package/src/index.ts +22 -5
  48. package/src/integration-interface.ts +118 -0
  49. package/src/integrations-store.ts +84 -0
  50. package/src/jwt.ts +233 -65
  51. package/src/registry.ts +23 -2
  52. package/src/secret-collection.ts +66 -0
  53. package/src/server.ts +268 -647
  54. package/src/types.ts +28 -2
  55. package/dist/slack-oauth.d.ts +0 -27
  56. package/dist/slack-oauth.d.ts.map +0 -1
  57. package/dist/slack-oauth.js +0 -48
  58. package/dist/slack-oauth.js.map +0 -1
  59. package/dist/web-pages.d.ts +0 -8
  60. package/dist/web-pages.d.ts.map +0 -1
  61. package/dist/web-pages.js +0 -169
  62. package/dist/web-pages.js.map +0 -1
  63. package/src/slack-oauth.ts +0 -66
  64. package/src/web-pages.ts +0 -178
package/src/server.ts CHANGED
@@ -1,28 +1,28 @@
1
1
  /**
2
2
  * Agent Server (MCP over HTTP)
3
3
  *
4
- * JSON-RPC server implementing the MCP protocol for agent interaction.
5
- * Compatible with atlas-environments and any MCP client.
4
+ * Minimal JSON-RPC server implementing the MCP protocol for agent interaction.
5
+ * Handles only core SDK concerns:
6
+ * - MCP protocol (initialize, tools/list, tools/call)
7
+ * - Agent registry routing (call_agent, list_agents)
8
+ * - Auth resolution (Bearer tokens, root key, JWT)
9
+ * - OAuth2 token exchange (client_credentials)
10
+ * - Health check
11
+ * - CORS
6
12
  *
7
- * MCP Methods:
8
- * - initialize → Protocol handshake
9
- * - tools/list → List available MCP tools (call_agent, list_agents)
10
- * - tools/call → Execute an MCP tool
13
+ * Application-specific routes (web UI, OAuth callbacks, tenant management)
14
+ * should be built on top using the exported `fetch` handler.
11
15
  *
12
- * MCP Tools exposed:
13
- * - call_agent → Execute a tool on a registered agent
14
- * - list_agents → List registered agents and their tools
16
+ * @example
17
+ * ```typescript
18
+ * // Standalone usage
19
+ * const server = createAgentServer(registry, { port: 3000 });
20
+ * await server.start();
15
21
  *
16
- * Additional endpoints:
17
- * - POST /oauth/token → OAuth2 client_credentials (when @auth registered)
18
- * - GET /health → Health check
19
- *
20
- * Auth Integration:
21
- * When an `@auth` agent is registered, the server automatically:
22
- * - Validates Bearer tokens on requests
23
- * - Resolves tokens to identity + scopes
24
- * - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
25
- * - Recognizes the root key for admin access
22
+ * // Composable with any HTTP framework
23
+ * const server = createAgentServer(registry);
24
+ * app.all('/mcp/*', (req) => server.fetch(req));
25
+ * ```
26
26
  */
27
27
 
28
28
  import type { AuthStore } from "./agent-definitions/auth.js";
@@ -31,17 +31,10 @@ import {
31
31
  processSecretParams,
32
32
  } from "./agent-definitions/secrets.js";
33
33
  import { verifyJwt } from "./jwt.js";
34
+ import type { SigningKey } from "./jwt.js";
35
+ import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
34
36
  import type { AgentRegistry } from "./registry.js";
35
37
  import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
36
- import { renderLoginPage, renderDashboardPage, renderTenantPage } from "./web-pages.js";
37
- import { slackAuthUrl, exchangeSlackCode, getSlackProfile, type SlackOAuthConfig } from "./slack-oauth.js";
38
-
39
-
40
- function resolveBaseUrl(req: Request, url: URL): string {
41
- const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
42
- const host = req.headers.get("x-forwarded-host") || url.host;
43
- return `${proto}://${host}`;
44
- }
45
38
 
46
39
  // ============================================
47
40
  // Server Types
@@ -60,9 +53,12 @@ export interface AgentServerOptions {
60
53
  serverName?: string;
61
54
  /** Server version reported in MCP initialize (default: '1.0.0') */
62
55
  serverVersion?: string;
63
-
64
56
  /** Secret store for handling secret: refs in tool params */
65
57
  secretStore?: SecretStore;
58
+ /** URLs of trusted registries for cross-registry JWT verification */
59
+ trustedIssuers?: string[];
60
+ /** Pre-generated signing key (if not provided, one is generated on start) */
61
+ signingKey?: SigningKey;
66
62
  }
67
63
 
68
64
  export interface AgentServer {
@@ -70,10 +66,12 @@ export interface AgentServer {
70
66
  start(): Promise<void>;
71
67
  /** Stop the server */
72
68
  stop(): Promise<void>;
73
- /** Handle a request (for custom integrations) */
69
+ /** Handle a request (for custom integrations / framework composition) */
74
70
  fetch(req: Request): Promise<Response>;
75
71
  /** Get the server URL (only available after start) */
76
72
  url: string | null;
73
+ /** The agent registry this server uses */
74
+ registry: AgentRegistry;
77
75
  }
78
76
 
79
77
  // ============================================
@@ -95,79 +93,24 @@ interface JsonRpcResponse {
95
93
  }
96
94
 
97
95
  // ============================================
98
- // Auth Types
96
+ // Auth Types (exported for use by custom routes)
99
97
  // ============================================
100
98
 
101
- interface AuthConfig {
99
+ export interface AuthConfig {
102
100
  store: AuthStore;
103
101
  rootKey: string;
104
102
  tokenTtl: number;
105
103
  }
106
104
 
107
- interface ResolvedAuth {
105
+ export interface ResolvedAuth {
108
106
  callerId: string;
109
107
  callerType: "agent" | "user" | "system";
110
108
  scopes: string[];
111
109
  isRoot: boolean;
112
110
  }
113
111
 
114
-
115
112
  // ============================================
116
- // Secrets Collection (one-time tokens)
117
- // ============================================
118
-
119
-
120
- function escHtml(s: string): string {
121
- return s.replace(/&/g,"\&amp;").replace(/</g,"\&lt;").replace(/>/g,"\&gt;").replace(/"/g,"\&quot;");
122
- }
123
-
124
- function renderSecretForm(token: string, pending: PendingCollection, baseUrl: string): string {
125
- const fields = pending.fields.map(f => `
126
- <div class="field">
127
- <label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
128
- ${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
129
- <input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
130
- </div>`).join("");
131
-
132
- return `<!DOCTYPE html>
133
- <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
134
- <style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:32px;max-width:480px;width:100%}.header{display:flex;align-items:center;gap:12px;margin-bottom:8px}.lock{font-size:24px}h1{font-size:20px;font-weight:600}.subtitle{color:#8b949e;font-size:14px;margin-bottom:24px}.shield{display:inline-flex;align-items:center;gap:4px;background:#1a2332;border:1px solid #1f6feb33;color:#58a6ff;font-size:12px;padding:2px 8px;border-radius:12px;margin-bottom:20px}label{display:block;font-size:14px;font-weight:500;margin-bottom:6px}.desc{font-size:12px;color:#8b949e;margin-bottom:4px}.badge{background:#3d1f00;color:#f0883e;font-size:10px;padding:1px 6px;border-radius:4px}.req{color:#f85149}input{width:100%;padding:10px 12px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:14px;margin-bottom:16px;outline:none}input:focus{border-color:#58a6ff;box-shadow:0 0 0 3px #1f6feb33}button{width:100%;padding:12px;background:#238636;border:none;border-radius:6px;color:#fff;font-size:14px;font-weight:600;cursor:pointer}button:hover{background:#2ea043}button:disabled{opacity:.5;cursor:not-allowed}.footer{text-align:center;margin-top:16px;font-size:12px;color:#484f58}.error{background:#3d1418;border:1px solid #f8514966;color:#f85149;padding:10px 12px;border-radius:6px;font-size:13px;margin-bottom:16px;display:none}.ok{text-align:center;padding:40px 0}.ok .icon{font-size:48px;margin-bottom:12px}.ok h2{font-size:18px;margin-bottom:8px;color:#3fb950}.ok p{color:#8b949e;font-size:14px}.field{position:relative}</style></head><body>
135
- <div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
136
- <p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
137
- <div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
138
- <form id="f">${fields}<button type="submit">Submit Securely</button></form>
139
- <p class="footer">Expires in 10 minutes</p></div>
140
- <div class="card ok" id="ok" style="display:none"><div class="icon">✅</div><h2>Done</h2><p>Credentials stored securely. You can close this window.</p></div>
141
- <script>document.getElementById("f").addEventListener("submit",async e=>{e.preventDefault();const b=e.target.querySelector("button");b.disabled=true;b.textContent="Submitting...";try{const fd=new FormData(e.target),vals=Object.fromEntries(fd.entries());const r=await fetch("${baseUrl}/secrets/collect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:"${token}",values:vals})});const d=await r.json();if(d.success){document.getElementById("fc").style.display="none";document.getElementById("ok").style.display="block";}else throw new Error(d.error?.message||JSON.stringify(d));}catch(err){const el=document.getElementById("err");el.textContent=err.message;el.style.display="block";b.disabled=false;b.textContent="Submit Securely";}});</script></body></html>`;
142
- }
143
-
144
- export interface PendingCollection {
145
- /** Partial params already provided by agent */
146
- params: Record<string, unknown>;
147
- /** Target agent + tool to call after collection */
148
- agent: string;
149
- tool: string;
150
- /** Auth context from original request */
151
- auth: ResolvedAuth | null;
152
- /** Fields the form needs to collect */
153
- fields: Array<{ name: string; description?: string; secret: boolean; required: boolean }>;
154
- /** Created timestamp for expiry */
155
- createdAt: number;
156
- }
157
-
158
- export const pendingCollections = new Map<string, PendingCollection>();
159
-
160
- export function generateCollectionToken(): string {
161
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
162
- let token = "sc_";
163
- for (let i = 0; i < 32; i++) {
164
- token += chars[Math.floor(Math.random() * chars.length)];
165
- }
166
- return token;
167
- }
168
-
169
- // ============================================
170
- // Helpers
113
+ // HTTP Helpers
171
114
  // ============================================
172
115
 
173
116
  function jsonResponse(data: unknown, status = 200): Response {
@@ -182,10 +125,22 @@ function corsHeaders(): Record<string, string> {
182
125
  "Access-Control-Allow-Origin": "*",
183
126
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
184
127
  "Access-Control-Allow-Headers":
185
- "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Agent-Id, X-Atlas-Session-Id",
128
+ "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Actor-Type",
186
129
  };
187
130
  }
188
131
 
132
+ function addCors(res: Response): Response {
133
+ const headers = new Headers(res.headers);
134
+ for (const [k, v] of Object.entries(corsHeaders())) {
135
+ headers.set(k, v);
136
+ }
137
+ return new Response(res.body, { status: res.status, headers });
138
+ }
139
+
140
+ // ============================================
141
+ // JSON-RPC Helpers
142
+ // ============================================
143
+
189
144
  function jsonRpcSuccess(id: unknown, result: unknown): JsonRpcResponse {
190
145
  return { jsonrpc: "2.0", id, result };
191
146
  }
@@ -218,10 +173,10 @@ function mcpResult(value: unknown, isError = false) {
218
173
  }
219
174
 
220
175
  // ============================================
221
- // Auth Detection
176
+ // Auth Detection & Resolution
222
177
  // ============================================
223
178
 
224
- function detectAuth(registry: AgentRegistry): AuthConfig | null {
179
+ export function detectAuth(registry: AgentRegistry): AuthConfig | null {
225
180
  const authAgent = registry.get("@auth") as
226
181
  | (AgentDefinition & {
227
182
  __authStore?: AuthStore;
@@ -239,9 +194,10 @@ function detectAuth(registry: AgentRegistry): AuthConfig | null {
239
194
  };
240
195
  }
241
196
 
242
- async function resolveAuth(
197
+ export async function resolveAuth(
243
198
  req: Request,
244
199
  authConfig: AuthConfig,
200
+ jwksOptions?: { signingKeys?: SigningKey[]; trustedIssuers?: string[] },
245
201
  ): Promise<ResolvedAuth | null> {
246
202
  const authHeader = req.headers.get("Authorization");
247
203
  if (!authHeader) return null;
@@ -249,6 +205,7 @@ async function resolveAuth(
249
205
  const [scheme, credential] = authHeader.split(" ", 2);
250
206
  if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
251
207
 
208
+ // Root key check
252
209
  if (credential === authConfig.rootKey) {
253
210
  return {
254
211
  callerId: "root",
@@ -258,12 +215,47 @@ async function resolveAuth(
258
215
  };
259
216
  }
260
217
 
261
- // Try JWT verification first (stateless)
262
- // JWT is signed with the client's secret hash
263
- // Decode payload to get client_id, look up client, verify signature
218
+ // Try ES256 verification against own signing keys
264
219
  const parts = credential.split(".");
220
+ if (parts.length === 3 && jwksOptions?.signingKeys?.length) {
221
+ for (const key of jwksOptions.signingKeys) {
222
+ try {
223
+ const verified = await verifyJwtLocal(credential, key.publicKey);
224
+ if (verified) {
225
+ return {
226
+ callerId: verified.sub ?? verified.name ?? "unknown",
227
+ callerType: "agent",
228
+ scopes: verified.scopes ?? ["*"],
229
+ isRoot: false,
230
+ };
231
+ }
232
+ } catch {
233
+ continue;
234
+ }
235
+ }
236
+ }
237
+
238
+ // Try trusted issuers (remote JWKS verification)
239
+ if (parts.length === 3 && jwksOptions?.trustedIssuers?.length) {
240
+ for (const issuer of jwksOptions.trustedIssuers) {
241
+ try {
242
+ const verified = await verifyJwtFromIssuer(credential, issuer);
243
+ if (verified) {
244
+ return {
245
+ callerId: verified.sub ?? verified.name ?? "unknown",
246
+ callerType: "agent",
247
+ scopes: verified.scopes ?? ["*"],
248
+ isRoot: false,
249
+ };
250
+ }
251
+ } catch {
252
+ continue;
253
+ }
254
+ }
255
+ }
256
+
257
+ // Try HMAC JWT verification (legacy, stateless)
265
258
  if (parts.length === 3) {
266
- // Looks like a JWT - decode payload to get client_id
267
259
  try {
268
260
  const payloadB64 = parts[1];
269
261
  const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
@@ -275,7 +267,6 @@ async function resolveAuth(
275
267
  };
276
268
 
277
269
  if (payload.sub) {
278
- // Look up client to get the signing secret (secret hash)
279
270
  const client = await authConfig.store.getClient(payload.sub);
280
271
  if (client) {
281
272
  const verified = await verifyJwt(credential, client.clientSecretHash);
@@ -307,7 +298,7 @@ async function resolveAuth(
307
298
  };
308
299
  }
309
300
 
310
- function canSeeAgent(
301
+ export function canSeeAgent(
311
302
  agent: AgentDefinition,
312
303
  auth: ResolvedAuth | null,
313
304
  ): boolean {
@@ -344,11 +335,11 @@ function getToolDefinitions() {
344
335
  },
345
336
  path: {
346
337
  type: "string",
347
- description: "Agent path (e.g. '@registry')",
338
+ description: "Agent path (e.g., '@my-agent')",
348
339
  },
349
340
  tool: {
350
341
  type: "string",
351
- description: "Tool name to call (for execute_tool)",
342
+ description: "Tool name to call",
352
343
  },
353
344
  params: {
354
345
  type: "object",
@@ -364,7 +355,8 @@ function getToolDefinitions() {
364
355
  },
365
356
  {
366
357
  name: "list_agents",
367
- description: "List all registered agents and their available tools.",
358
+ description:
359
+ "List all registered agents and their available tools.",
368
360
  inputSchema: {
369
361
  type: "object",
370
362
  properties: {},
@@ -391,13 +383,15 @@ export function createAgentServer(
391
383
  secretStore,
392
384
  } = options;
393
385
 
394
- let serverInstance: ReturnType<typeof Bun.serve> | null = null;
395
- let serverUrl: string | null = null;
386
+ // Signing keys for JWKS-based auth
387
+ const serverSigningKeys: SigningKey[] = [];
388
+ const configTrustedIssuers: string[] = options.trustedIssuers ?? [];
396
389
 
397
390
  const authConfig = detectAuth(registry);
391
+ let serverInstance: ReturnType<typeof Bun.serve> | null = null;
398
392
 
399
393
  // ──────────────────────────────────────────
400
- // MCP JSON-RPC handler
394
+ // JSON-RPC handler
401
395
  // ──────────────────────────────────────────
402
396
 
403
397
  async function handleJsonRpc(
@@ -405,24 +399,21 @@ export function createAgentServer(
405
399
  auth: ResolvedAuth | null,
406
400
  ): Promise<JsonRpcResponse> {
407
401
  switch (request.method) {
408
- // MCP protocol handshake
409
402
  case "initialize":
410
403
  return jsonRpcSuccess(request.id, {
411
404
  protocolVersion: "2024-11-05",
412
- capabilities: { tools: {} },
405
+ capabilities: { tools: { listChanged: false } },
413
406
  serverInfo: { name: serverName, version: serverVersion },
414
407
  });
415
408
 
416
409
  case "notifications/initialized":
417
410
  return jsonRpcSuccess(request.id, {});
418
411
 
419
- // List MCP tools
420
412
  case "tools/list":
421
413
  return jsonRpcSuccess(request.id, {
422
414
  tools: getToolDefinitions(),
423
415
  });
424
416
 
425
- // Call an MCP tool
426
417
  case "tools/call": {
427
418
  const { name, arguments: args } = (request.params ?? {}) as {
428
419
  name: string;
@@ -433,7 +424,7 @@ export function createAgentServer(
433
424
  const result = await handleToolCall(name, args ?? {}, auth);
434
425
  return jsonRpcSuccess(request.id, result);
435
426
  } catch (err) {
436
- console.error("[server] Request error:", err);
427
+ console.error("[server] Tool call error:", err);
437
428
  return jsonRpcSuccess(
438
429
  request.id,
439
430
  mcpResult(
@@ -479,10 +470,8 @@ export function createAgentServer(
479
470
  }
480
471
 
481
472
  // Process secret params: resolve refs, store raw secrets
482
- // Auto-resolve secret:xxx refs in tool params before execution
483
473
  if ((req as any).params && secretStore) {
484
474
  const ownerId = auth?.callerId ?? "anonymous";
485
- // Find the tool schema to check for secret: true fields
486
475
  const agent = registry.get(req.path);
487
476
  const tool = agent?.tools.find((t) => t.name === (req as any).tool);
488
477
  const schema = tool?.inputSchema as any;
@@ -516,6 +505,7 @@ export function createAgentServer(
516
505
  const tv = t.visibility ?? "internal";
517
506
  if (auth?.isRoot) return true;
518
507
  if (tv === "public") return true;
508
+ if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous") return true;
519
509
  if (tv === "internal" && auth) return true;
520
510
  return false;
521
511
  })
@@ -530,7 +520,7 @@ export function createAgentServer(
530
520
  }
531
521
 
532
522
  // ──────────────────────────────────────────
533
- // OAuth2 token handler (unchanged)
523
+ // OAuth2 token handler
534
524
  // ──────────────────────────────────────────
535
525
 
536
526
  async function handleOAuthToken(req: Request): Promise<Response> {
@@ -576,551 +566,175 @@ export function createAgentServer(
576
566
  );
577
567
  }
578
568
 
579
- const client = await authConfig.store.validateClient(
580
- clientId,
581
- clientSecret,
582
- );
583
- if (!client) {
584
- return jsonResponse(
585
- {
586
- error: "invalid_client",
587
- error_description: "Invalid client credentials",
588
- },
589
- 401,
590
- );
591
- }
592
-
593
- // Delegate to @auth agent's token tool which generates proper JWTs
594
- const tokenResult = await registry.call({
595
- action: "execute_tool",
596
- path: "@auth",
597
- tool: "token",
598
- params: {
599
- grantType: "client_credentials",
600
- clientId,
601
- clientSecret,
602
- },
603
- context: {
604
- tenantId: "default",
605
- agentPath: "@auth",
606
- callerId: "oauth_endpoint",
569
+ try {
570
+ const result = await registry.call({
571
+ action: "execute_tool",
572
+ path: "@auth",
573
+ tool: "token",
574
+ params: { clientId, clientSecret },
607
575
  callerType: "system",
608
- },
609
- } as any);
576
+ });
577
+
578
+ const tokenResult = (result as any)?.result;
579
+ if (!tokenResult?.accessToken) {
580
+ return jsonResponse(
581
+ { error: "invalid_client", error_description: "Authentication failed" },
582
+ 401,
583
+ );
584
+ }
610
585
 
611
- // Extract the result - registry.call returns { success, result: { accessToken, tokenType, expiresIn, scopes } }
612
- const callResponse = tokenResult as any;
613
- if (!callResponse.success) {
614
- return jsonResponse({ error: "token_generation_failed", error_description: callResponse.error ?? "Unknown error" }, 500);
586
+ return jsonResponse({
587
+ access_token: tokenResult.accessToken,
588
+ token_type: "Bearer",
589
+ expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
590
+ refresh_token: tokenResult.refreshToken,
591
+ });
592
+ } catch (err) {
593
+ console.error("[oauth] Token error:", err);
594
+ return jsonResponse(
595
+ { error: "server_error", error_description: "Token exchange failed" },
596
+ 500,
597
+ );
615
598
  }
616
- const tokenData = callResponse.result;
617
-
618
- // accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
619
- const accessToken = tokenData.accessToken?.$agent_type === "secret"
620
- ? tokenData.accessToken.value
621
- : tokenData.accessToken;
622
-
623
- return jsonResponse({
624
- access_token: accessToken,
625
- token_type: tokenData.tokenType ?? "Bearer",
626
- expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
627
- scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
628
- });
629
599
  }
630
600
 
631
601
  // ──────────────────────────────────────────
632
- // HTTP request handler
633
- // ─��────────────────────────────────────────
602
+ // Main fetch handler
603
+ // ──────────────────────────────────────────
634
604
 
635
605
  async function fetch(req: Request): Promise<Response> {
636
- const url = new URL(req.url);
637
- const path = url.pathname.replace(basePath, "") || "/";
638
-
639
- // CORS preflight
640
- if (cors && req.method === "OPTIONS") {
641
- return new Response(null, { status: 204, headers: corsHeaders() });
642
- }
643
-
644
- const addCors = (response: Response): Response => {
645
- if (!cors) return response;
646
- const headers = new Headers(response.headers);
647
- for (const [key, value] of Object.entries(corsHeaders())) {
648
- headers.set(key, value);
649
- }
650
- return new Response(response.body, {
651
- status: response.status,
652
- statusText: response.statusText,
653
- headers,
654
- });
655
- };
656
-
657
- const auth = authConfig ? await resolveAuth(req, authConfig) : null;
658
-
659
606
  try {
660
- // MCP endpoint: POST / or POST /mcp
661
- if ((path === "/" || path === "/mcp") && req.method === "POST") {
662
- const body = (await req.json()) as JsonRpcRequest;
663
- const response = await handleJsonRpc(body, auth);
664
- return addCors(jsonResponse(response));
665
- }
666
-
667
- // OAuth2 token endpoint
668
- if (path === "/oauth/token" && req.method === "POST") {
669
- return addCors(await handleOAuthToken(req));
670
- }
671
-
672
- // Health check
673
- if (path === "/health" && req.method === "GET") {
674
- return addCors(jsonResponse({ status: "ok" }));
675
- }
676
-
677
- // Backwards compat: GET /list (returns agents directly)
678
- if (path === "/list" && req.method === "GET") {
679
- const agents = registry.list();
680
- const visible = agents.filter((agent) => canSeeAgent(agent, auth));
681
- return addCors(
682
- jsonResponse({
683
- success: true,
684
- agents: visible.map((agent) => ({
685
- path: agent.path,
686
- name: agent.config?.name,
687
- description: agent.config?.description,
688
- supportedActions: agent.config?.supportedActions,
689
- integration: agent.config?.integration || null,
690
- tools: agent.tools
691
- .filter((t) => {
692
- const tv = t.visibility ?? "internal";
693
- if (auth?.isRoot) return true;
694
- if (tv === "public") return true;
695
- if (tv === "internal" && auth) return true;
696
- return false;
697
- })
698
- .map((t) => t.name),
699
- })),
700
- }),
701
- );
702
- }
703
-
704
-
705
- // GET /integrations/callback/:provider - OAuth callback
706
- if (path.startsWith("/integrations/callback/") && req.method === "GET") {
707
- const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
708
- if (!provider) {
709
- return addCors(jsonResponse({ error: "Missing provider" }, 400));
710
- }
711
-
712
- const url = new URL(req.url);
713
- const code = url.searchParams.get("code");
714
- const state = url.searchParams.get("state");
715
- const oauthError = url.searchParams.get("error");
716
- const errorDescription = url.searchParams.get("error_description");
717
-
718
- if (oauthError) {
719
- return new Response(
720
- `<html><body><h1>Authorization Failed</h1><p>${errorDescription ?? oauthError}</p></body></html>`,
721
- { status: 400, headers: { "Content-Type": "text/html", ...corsHeaders() } },
722
- );
723
- }
724
-
725
- if (!code) {
726
- return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
727
- }
728
-
729
- // Call handle_oauth_callback tool on @integrations
730
- try {
731
- await registry.call({
732
- action: "execute_tool",
733
- path: "@integrations",
734
- tool: "handle_oauth_callback",
735
- params: { provider, code, state: state ?? undefined },
736
- context: {
737
- tenantId: "default",
738
- agentPath: "@integrations",
739
- callerId: "oauth_callback",
740
- callerType: "system",
741
- },
742
- } as any);
743
-
744
- // Parse redirect URL from state
745
- let redirectUrl = "/";
746
- if (state) {
747
- try {
748
- const parsed = JSON.parse(state);
749
- if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
750
- } catch {}
751
- }
752
-
753
- const sep = redirectUrl.includes("?") ? "&" : "?";
754
- return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
755
- } catch (err) {
756
- return new Response(
757
- `<html><body><h1>Connection Failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`,
758
- { status: 500, headers: { "Content-Type": "text/html", ...corsHeaders() } },
759
- );
760
- }
761
- }
762
-
763
-
764
- // GET /secrets/form/:token - Serve hosted secrets form
765
- if (path.startsWith("/secrets/form/") && req.method === "GET") {
766
- const token = path.split("/").pop() ?? "";
767
- const pending = pendingCollections.get(token);
768
- if (!pending) {
769
- return addCors(new Response("Invalid or expired form link", { status: 404 }));
770
- }
771
- if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
772
- pendingCollections.delete(token);
773
- return addCors(new Response("Form link expired", { status: 410 }));
774
- }
775
- const reqUrl = new URL(req.url); const baseUrl = resolveBaseUrl(req, reqUrl);
776
- const html = renderSecretForm(token, pending, baseUrl);
777
- return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
778
- }
779
-
780
- // POST /secrets/collect - Submit collected secrets and auto-forward to tool
781
- if (path === "/secrets/collect" && req.method === "POST") {
782
- const body = (await req.json()) as {
783
- token: string;
784
- values: Record<string, string>;
785
- };
786
-
787
- const pending = pendingCollections.get(body.token);
788
- if (!pending) {
789
- return addCors(
790
- jsonResponse({ error: "Invalid or expired collection token" }, 400),
791
- );
792
- }
793
-
794
- // One-time use
795
- pendingCollections.delete(body.token);
796
-
797
- // Check expiry (10 min)
798
- if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
799
- return addCors(
800
- jsonResponse({ error: "Collection token expired" }, 400),
801
- );
802
- }
803
-
804
- // Encrypt secret values and store as refs
805
- const mergedParams = { ...pending.params };
806
- for (const [fieldName, value] of Object.entries(body.values)) {
807
- const fieldDef = pending.fields.find((f) => f.name === fieldName);
808
- if (fieldDef?.secret && secretStore) {
809
- // Store encrypted, get ref
810
- const ownerId = pending.auth?.callerId ?? "anonymous";
811
- const secretId = await secretStore.store(value, ownerId);
812
- mergedParams[fieldName] = `secret:${secretId}`;
813
- } else {
814
- mergedParams[fieldName] = value;
815
- }
816
- }
607
+ const url = new URL(req.url);
608
+ const path = url.pathname.replace(basePath, "") || "/";
817
609
 
818
- // Auto-forward to the target tool
819
- const callRequest = {
820
- action: "execute_tool" as const,
821
- path: pending.agent,
822
- tool: pending.tool,
823
- params: mergedParams,
824
- };
825
-
826
- const toolCtx = {
827
- tenantId: "default",
828
- agentPath: pending.agent,
829
- callerId: pending.auth?.callerId ?? "anonymous",
830
- callerType: pending.auth?.callerType ?? ("system" as const),
831
- };
832
-
833
- const result = await registry.call({
834
- ...callRequest,
835
- context: toolCtx,
836
- } as any);
837
-
838
- return addCors(jsonResponse({ success: true, result }));
610
+ // CORS preflight
611
+ if (cors && req.method === "OPTIONS") {
612
+ return new Response(null, { status: 204, headers: corsHeaders() });
839
613
  }
840
614
 
841
-
842
- // --- Web pages (plain HTML, served from same server) ---
843
- const htmlRes = (body: string) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
844
- const reqUrl = new URL(req.url);
845
- const baseUrl = resolveBaseUrl(req, reqUrl);
846
-
847
- const slackConfig: SlackOAuthConfig | null =
848
- process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
849
- ? {
850
- clientId: process.env.SLACK_CLIENT_ID,
851
- clientSecret: process.env.SLACK_CLIENT_SECRET,
852
- redirectUri: `${baseUrl}/auth/slack/callback`,
615
+ // Resolve auth for all requests
616
+ const auth = authConfig ? await resolveAuth(req, authConfig, {
617
+ signingKeys: serverSigningKeys,
618
+ trustedIssuers: configTrustedIssuers,
619
+ }) : null;
620
+
621
+ // Also check header-based identity (for proxied requests)
622
+ const headerAuth: ResolvedAuth | null = !auth
623
+ ? (() => {
624
+ const actorId = req.headers.get("X-Atlas-Actor-Id");
625
+ const actorType = req.headers.get("X-Atlas-Actor-Type");
626
+ if (actorId) {
627
+ return {
628
+ callerId: actorId,
629
+ callerType: (actorType as any) ?? "agent",
630
+ scopes: ["*"],
631
+ isRoot: false,
632
+ };
853
633
  }
854
- : null;
855
-
856
- // Helper: read session from cookie
857
- function getSession(r: Request): Record<string, any> | null {
858
- const c = r.headers.get("Cookie") || "";
859
- const m = c.match(/s_session=([^;]+)/);
860
- if (!m) return null;
861
- try { return JSON.parse(Buffer.from(m[1], "base64url").toString()); }
862
- catch { return null; }
863
- }
634
+ return null;
635
+ })()
636
+ : null;
864
637
 
865
- // Helper: generate JWT from client credentials
866
- async function generateMcpToken(): Promise<string> {
867
- const clientRes = await registry.call({ action: "execute_tool", path: "@auth", tool: "create_client", callerType: "system", params: {
868
- name: "mcp-" + Date.now(),
869
- scopes: ["*"],
870
- }} as any) as any;
871
- const cid = clientRes?.result?.clientId;
872
- const csec = clientRes?.result?.clientSecret;
873
- if (!cid || !csec) throw new Error("Failed to create client: " + JSON.stringify(clientRes));
874
-
875
- const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
876
- method: "POST",
877
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
878
- body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
879
- });
880
- const tokenData = await tokenRes.json() as any;
881
- if (!tokenData.access_token) throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
882
- return tokenData.access_token;
883
- }
638
+ const effectiveAuth = auth ?? headerAuth;
884
639
 
885
- // Helper: set session cookie and redirect
886
- function sessionRedirect(location: string, session: Record<string, any>): Response {
887
- const data = Buffer.from(JSON.stringify(session)).toString("base64url");
888
- return new Response(null, {
889
- status: 302,
890
- headers: {
891
- Location: location,
892
- "Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
893
- },
894
- });
895
- }
896
-
897
- // GET / — login page (or redirect to dashboard if session exists)
898
- if (path === "/" && req.method === "GET") {
899
- const session = getSession(req);
900
- if (session?.token) return Response.redirect(`${baseUrl}/dashboard`, 302);
901
- return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
902
- }
903
-
904
- // GET /auth/slack — start Slack OAuth
905
- if (path === "/auth/slack" && req.method === "GET") {
906
- if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
907
- return Response.redirect(slackAuthUrl(slackConfig), 302);
640
+ // ── POST / MCP JSON-RPC ──
641
+ if (path === "/" && req.method === "POST") {
642
+ const body = (await req.json()) as JsonRpcRequest;
643
+ const result = await handleJsonRpc(body, effectiveAuth);
644
+ return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
908
645
  }
909
646
 
910
- // GET /auth/slack/callback handle Slack OAuth callback
911
- if (path === "/auth/slack/callback" && req.method === "GET") {
912
- if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
913
- const authCode = reqUrl.searchParams.get("code");
914
- const authError = reqUrl.searchParams.get("error");
915
- if (authError || !authCode) return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
916
-
917
- try {
918
- const tokens = await exchangeSlackCode(authCode, slackConfig);
919
- const profile = await getSlackProfile(tokens.access_token);
920
- const teamId = profile["https://slack.com/team_id"] || "";
921
- const teamName = profile["https://slack.com/team_name"] || "";
922
-
923
- // Check if user already exists
924
- console.log("[auth] Looking up slack user:", profile.sub, profile.email);
925
- const existing = await registry.call({
926
- action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
927
- params: { provider: "slack", providerUserId: profile.sub },
928
- } as any) as any;
929
- console.log("[auth] resolve_identity:", JSON.stringify(existing));
930
-
931
- if (existing?.result?.found && existing?.result?.user?.tenantId) {
932
- // Returning user — generate token and go to dashboard
933
- console.log("[auth] Returning user, generating token...");
934
- const mcpToken = await generateMcpToken();
935
- return sessionRedirect(`${baseUrl}/dashboard`, {
936
- userId: existing.result.user.id,
937
- tenantId: existing.result.user.tenantId,
938
- email: existing.result.user.email,
939
- name: existing.result.user.name,
940
- token: mcpToken,
941
- });
942
- }
943
-
944
- // Check if Slack team already has a tenant
945
- if (teamId) {
946
- console.log("[auth] Checking tenant_identities for team:", teamId);
947
- try {
948
- // Direct DB query via the auth store's underlying connection
949
- // We'll use a simple fetch to our own MCP endpoint to call @db-connections
950
- // Actually, simpler: just query the DB directly via the server's context
951
- // For now, use registry.call to a custom tool or direct SQL
952
- // Simplest: call @auth list_tenants and check metadata
953
- // Even simpler: direct SQL via globalThis.fetch to ourselves
954
- const dbUrl = process.env.DATABASE_URL;
955
- if (dbUrl) {
956
- const { default: postgres } = await import("postgres");
957
- const sql = postgres(dbUrl);
958
- const rows = await sql`SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
959
- await sql.end();
960
- if (rows.length > 0) {
961
- const existingTenantId = rows[0].tenant_id;
962
- console.log("[auth] Found existing tenant for team:", existingTenantId);
963
-
964
- // Create user on existing tenant
965
- const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
966
- email: profile.email, name: profile.name, tenantId: existingTenantId,
967
- }} as any) as any;
968
- const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
969
- console.log("[auth] Created user on existing tenant:", newUserId);
970
-
971
- // Link identity
972
- if (newUserId) {
973
- await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
974
- userId: newUserId, provider: "slack", providerUserId: profile.sub,
975
- email: profile.email, name: profile.name,
976
- metadata: { slackTeamId: teamId, slackTeamName: teamName },
977
- }} as any);
978
- }
979
-
980
- // Generate token and go to dashboard
981
- const mcpToken = await generateMcpToken();
982
- return sessionRedirect(`${baseUrl}/dashboard`, {
983
- userId: newUserId, tenantId: existingTenantId,
984
- email: profile.email, name: profile.name, token: mcpToken,
985
- });
986
- }
987
- }
988
- } catch (e: any) {
989
- console.error("[auth] tenant_identity lookup error:", e.message);
990
- }
991
- }
992
-
993
- // New user — redirect to setup
994
- return sessionRedirect(`${baseUrl}/setup`, {
995
- email: profile.email,
996
- name: profile.name,
997
- picture: profile.picture,
998
- slackUserId: profile.sub,
999
- slackTeamId: teamId,
1000
- slackTeamName: teamName,
1001
- });
1002
- } catch (err: any) {
1003
- console.error("[auth] callback error:", err);
1004
- return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
1005
- }
647
+ // ── POST /oauth/token OAuth2 client_credentials ──
648
+ if (path === "/oauth/token" && req.method === "POST") {
649
+ const res = await handleOAuthToken(req);
650
+ return cors ? addCors(res) : res;
1006
651
  }
1007
652
 
1008
- // GET /setup tenant creation page
1009
- if (path === "/setup" && req.method === "GET") {
1010
- const session = getSession(req);
1011
- if (!session?.email) return Response.redirect(`${baseUrl}/`, 302);
1012
- return htmlRes(renderTenantPage(baseUrl, session.email, session.name || ""));
653
+ // ── GET /health Health check ──
654
+ if (path === "/health" && req.method === "GET") {
655
+ const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
656
+ return cors ? addCors(res) : res;
1013
657
  }
1014
658
 
1015
- // POST /setup create tenant + user + link identity + generate token
1016
- if (path === "/setup" && req.method === "POST") {
1017
- try {
1018
- const body = await req.json() as { email?: string; tenant?: string; name?: string };
1019
- const session = getSession(req);
1020
- console.log("[setup] body:", JSON.stringify(body), "session:", JSON.stringify(session));
1021
-
1022
- // 1. Create tenant
1023
- const tenantRes = await registry.call({ action: "execute_tool", path: "@auth", callerType: "system", tool: "create_tenant", params: { name: body.tenant } } as any) as any;
1024
- const tenantId = tenantRes?.result?.tenantId;
1025
- if (!tenantId) return addCors(jsonResponse({ error: "Failed to create tenant" }, 400));
1026
- console.log("[setup] tenant created:", tenantId);
1027
-
1028
- // 2. Create user
1029
- const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: { email: body.email, name: session?.name, tenantId } } as any) as any;
1030
- const userId = userRes?.result?.id || userRes?.result?.user?.id;
1031
- console.log("[setup] user created:", userId);
1032
-
1033
- // 2b. Link tenant to Slack team
1034
- if (session?.slackTeamId) {
1035
- try {
1036
- const dbUrl = process.env.DATABASE_URL;
1037
- if (dbUrl) {
1038
- const { default: postgres } = await import("postgres");
1039
- const sql = postgres(dbUrl);
1040
- const id = "ti_" + Math.random().toString(36).slice(2, 14);
1041
- await sql`INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
1042
- await sql.end();
1043
- console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
1044
- }
1045
- } catch (e: any) { console.error("[setup] tenant_identity insert error:", e.message); }
1046
- }
1047
-
1048
- // 3. Link Slack identity
1049
- if (session?.slackUserId && userId) {
1050
- console.log("[setup] linking slack identity:", session.slackUserId);
1051
- const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
1052
- userId,
1053
- provider: "slack",
1054
- providerUserId: session.slackUserId,
1055
- email: body.email,
1056
- name: session.name,
1057
- metadata:{ slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system" }} as any);
1058
- console.log("[setup] link_identity result:", JSON.stringify(linkRes));
1059
- }
1060
-
1061
- // 4. Generate MCP token
1062
- const mcpToken = await generateMcpToken();
1063
- console.log("[setup] token generated, length:", mcpToken.length);
1064
-
1065
- return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
1066
- } catch (err: any) {
1067
- console.error("[setup] error:", err);
1068
- return addCors(jsonResponse({ error: err.message }, 400));
1069
- }
659
+ // ── GET /.well-known/jwks.json JWKS public keys ──
660
+ if (path === "/.well-known/jwks.json" && req.method === "GET") {
661
+ const jwks = serverSigningKeys.length > 0
662
+ ? await buildJwks(serverSigningKeys)
663
+ : { keys: [] };
664
+ const res = jsonResponse(jwks);
665
+ return cors ? addCors(res) : res;
1070
666
  }
1071
667
 
1072
- // POST /logout clear session
1073
- if (path === "/logout" && req.method === "POST") {
1074
- return new Response(null, {
1075
- status: 302,
1076
- headers: {
1077
- Location: `${baseUrl}/`,
1078
- "Set-Cookie": "s_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
1079
- },
668
+ // ── GET /.well-known/configuration Server discovery ──
669
+ if (path === "/.well-known/configuration" && req.method === "GET") {
670
+ const baseUrl = new URL(req.url).origin;
671
+ const res = jsonResponse({
672
+ issuer: baseUrl,
673
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
674
+ token_endpoint: `${baseUrl}/oauth/token`,
675
+ agents_endpoint: `${baseUrl}/list`,
676
+ call_endpoint: baseUrl,
677
+ supported_grant_types: ["client_credentials"],
678
+ agents: registry.listPaths(),
1080
679
  });
680
+ return cors ? addCors(res) : res;
1081
681
  }
1082
682
 
1083
- // GET /dashboard show MCP URL and setup instructions
1084
- if (path === "/dashboard" && req.method === "GET") {
1085
- const session = getSession(req);
1086
- let token = session?.token || reqUrl.searchParams.get("token") || "";
1087
- if (!token) return Response.redirect(`${baseUrl}/`, 302);
1088
-
1089
- // Persist token in cookie
1090
- const sessData = Buffer.from(JSON.stringify({ ...session, token })).toString("base64url");
1091
- return new Response(renderDashboardPage(baseUrl, token, session || undefined), {
1092
- headers: {
1093
- "Content-Type": "text/html; charset=utf-8",
1094
- "Set-Cookie": `s_session=${sessData}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
1095
- },
1096
- });
683
+ // ── GET /list List agents (legacy endpoint) ──
684
+ if (path === "/list" && req.method === "GET") {
685
+ const agents = registry.list();
686
+ const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
687
+ const res = jsonResponse(
688
+ visible.map((agent) => ({
689
+ path: agent.path,
690
+ name: agent.config?.name,
691
+ description: agent.config?.description,
692
+ supportedActions: agent.config?.supportedActions,
693
+ integration: agent.config?.integration || null,
694
+ tools: agent.tools
695
+ .filter((t) => {
696
+ const tv = t.visibility ?? "internal";
697
+ if (effectiveAuth?.isRoot) return true;
698
+ if (tv === "public") return true;
699
+ if (tv === "internal" && effectiveAuth) return true;
700
+ return false;
701
+ })
702
+ .map((t) => ({
703
+ name: t.name,
704
+ description: t.description,
705
+ })),
706
+ })),
707
+ );
708
+ return cors ? addCors(res) : res;
1097
709
  }
1098
710
 
1099
- return addCors(
1100
- jsonResponse(
1101
- {
1102
- jsonrpc: "2.0",
1103
- id: null,
1104
- error: {
1105
- code: -32601,
1106
- message: `Not found: ${req.method} ${path}`,
1107
- },
711
+ // ── Not found ──
712
+ const res = jsonResponse(
713
+ {
714
+ jsonrpc: "2.0",
715
+ id: null,
716
+ error: {
717
+ code: -32601,
718
+ message: `Not found: ${req.method} ${path}`,
1108
719
  },
1109
- 404,
1110
- ),
720
+ },
721
+ 404,
1111
722
  );
723
+ return cors ? addCors(res) : res;
1112
724
  } catch (err) {
1113
725
  console.error("[server] Request error:", err);
1114
- return addCors(
1115
- jsonResponse(
1116
- {
1117
- jsonrpc: "2.0",
1118
- id: null,
1119
- error: { code: -32603, message: "Internal error" },
726
+ const res = jsonResponse(
727
+ {
728
+ jsonrpc: "2.0",
729
+ id: null,
730
+ error: {
731
+ code: -32603,
732
+ message: err instanceof Error ? err.message : "Internal error",
1120
733
  },
1121
- 500,
1122
- ),
734
+ },
735
+ 500,
1123
736
  );
737
+ return cors ? addCors(res) : res;
1124
738
  }
1125
739
  }
1126
740
 
@@ -1128,38 +742,45 @@ export function createAgentServer(
1128
742
  // Server lifecycle
1129
743
  // ──────────────────────────────────────────
1130
744
 
1131
- const server: AgentServer = {
1132
- async start(): Promise<void> {
1133
- if (serverInstance) throw new Error("Server is already running");
1134
-
1135
- serverInstance = Bun.serve({ port, hostname, fetch });
1136
- serverUrl = `http://${hostname}:${port}${basePath}`;
1137
-
1138
- console.log(`Agent server running at ${serverUrl}`);
1139
- console.log(" POST / - MCP JSON-RPC endpoint");
1140
- console.log(" POST /mcp - MCP JSON-RPC endpoint (alias)");
1141
- console.log(" GET /health - Health check");
1142
- if (authConfig) {
1143
- console.log(" POST /oauth/token - OAuth2 token endpoint");
1144
- console.log(" Auth: enabled");
745
+ return {
746
+ url: null,
747
+ registry,
748
+
749
+ async start() {
750
+ // Load or generate signing key for JWKS
751
+ if (options.signingKey) {
752
+ serverSigningKeys.push(options.signingKey);
753
+ } else if (authConfig?.store?.getSigningKeys) {
754
+ const stored = await authConfig.store.getSigningKeys() ?? [];
755
+ for (const exported of stored) {
756
+ serverSigningKeys.push(await importSigningKey(exported));
757
+ }
758
+ }
759
+ if (serverSigningKeys.length === 0) {
760
+ const key = await generateSigningKey();
761
+ serverSigningKeys.push(key);
762
+ if (authConfig?.store?.storeSigningKey) {
763
+ await authConfig.store.storeSigningKey(await exportSigningKey(key));
764
+ }
1145
765
  }
1146
- console.log(" MCP tools: call_agent, list_agents");
766
+
767
+ serverInstance = Bun.serve({
768
+ port,
769
+ hostname,
770
+ fetch,
771
+ });
772
+ (this as any).url = `http://${hostname}:${port}`;
773
+ console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
1147
774
  },
1148
775
 
1149
- async stop(): Promise<void> {
776
+ async stop() {
1150
777
  if (serverInstance) {
1151
778
  serverInstance.stop();
1152
779
  serverInstance = null;
1153
- serverUrl = null;
780
+ (this as any).url = null;
1154
781
  }
1155
782
  },
1156
783
 
1157
784
  fetch,
1158
-
1159
- get url(): string | null {
1160
- return serverUrl;
1161
- },
1162
785
  };
1163
-
1164
- return server;
1165
786
  }