@slashfi/agents-sdk 0.24.2 → 0.24.3
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/cjs/agent-definitions/auth.js +678 -0
- package/dist/cjs/agent-definitions/auth.js.map +1 -0
- package/dist/cjs/agent-definitions/integrations.js +1173 -0
- package/dist/cjs/agent-definitions/integrations.js.map +1 -0
- package/dist/cjs/agent-definitions/remote-registry.js +469 -0
- package/dist/cjs/agent-definitions/remote-registry.js.map +1 -0
- package/dist/cjs/agent-definitions/secrets.js +193 -0
- package/dist/cjs/agent-definitions/secrets.js.map +1 -0
- package/dist/cjs/agent-definitions/users.js +440 -0
- package/dist/cjs/agent-definitions/users.js.map +1 -0
- package/dist/cjs/build.js +162 -0
- package/dist/cjs/build.js.map +1 -0
- package/dist/cjs/callback/index.js +74 -0
- package/dist/cjs/callback/index.js.map +1 -0
- package/dist/cjs/client.js +193 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/codegen.js +1027 -0
- package/dist/cjs/codegen.js.map +1 -0
- package/dist/cjs/crypto.js +44 -0
- package/dist/cjs/crypto.js.map +1 -0
- package/dist/cjs/define-config.js +81 -0
- package/dist/cjs/define-config.js.map +1 -0
- package/dist/cjs/define.js +186 -0
- package/dist/cjs/define.js.map +1 -0
- package/dist/cjs/events.js +60 -0
- package/dist/cjs/events.js.map +1 -0
- package/dist/cjs/index.js +195 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/integration-interface.js +105 -0
- package/dist/cjs/integration-interface.js.map +1 -0
- package/dist/cjs/integrations-store.js +53 -0
- package/dist/cjs/integrations-store.js.map +1 -0
- package/dist/cjs/introspect.js +136 -0
- package/dist/cjs/introspect.js.map +1 -0
- package/dist/cjs/jsonc.js +74 -0
- package/dist/cjs/jsonc.js.map +1 -0
- package/dist/cjs/jwt.js +207 -0
- package/dist/cjs/jwt.js.map +1 -0
- package/dist/cjs/key-manager.js +161 -0
- package/dist/cjs/key-manager.js.map +1 -0
- package/dist/cjs/oidc-signin.js +141 -0
- package/dist/cjs/oidc-signin.js.map +1 -0
- package/dist/cjs/pack.js +256 -0
- package/dist/cjs/pack.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/registry-consumer.js +233 -0
- package/dist/cjs/registry-consumer.js.map +1 -0
- package/dist/cjs/registry.js +512 -0
- package/dist/cjs/registry.js.map +1 -0
- package/dist/cjs/secret-collection.js +42 -0
- package/dist/cjs/secret-collection.js.map +1 -0
- package/dist/cjs/serialized.js +45 -0
- package/dist/cjs/serialized.js.map +1 -0
- package/dist/cjs/server.js +974 -0
- package/dist/cjs/server.js.map +1 -0
- package/dist/cjs/test-utils/mock-oidc-server.js +99 -0
- package/dist/cjs/test-utils/mock-oidc-server.js.map +1 -0
- package/dist/cjs/types.js +8 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/validate.js +84 -0
- package/dist/cjs/validate.js.map +1 -0
- package/package.json +13 -5
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent Server (MCP over HTTP)
|
|
4
|
+
*
|
|
5
|
+
* Minimal JSON-RPC server implementing the MCP protocol for agent interaction.
|
|
6
|
+
* Handles only core SDK concerns:
|
|
7
|
+
* - MCP protocol (initialize, tools/list, tools/call)
|
|
8
|
+
* - Agent registry routing (call_agent, list_agents)
|
|
9
|
+
* - Auth resolution (Bearer tokens, root key, JWT)
|
|
10
|
+
* - OAuth2 token exchange (client_credentials)
|
|
11
|
+
* - Health check
|
|
12
|
+
* - CORS
|
|
13
|
+
*
|
|
14
|
+
* Application-specific routes (web UI, OAuth callbacks, tenant management)
|
|
15
|
+
* should be built on top using the exported `fetch` handler.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // Standalone usage
|
|
20
|
+
* const server = createAgentServer(registry, { port: 3000 });
|
|
21
|
+
* await server.start();
|
|
22
|
+
*
|
|
23
|
+
* // Composable with any HTTP framework
|
|
24
|
+
* const server = createAgentServer(registry);
|
|
25
|
+
* app.all('/mcp/*', (req) => server.fetch(req));
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.detectAuth = detectAuth;
|
|
30
|
+
exports.resolveAuth = resolveAuth;
|
|
31
|
+
exports.canSeeAgent = canSeeAgent;
|
|
32
|
+
exports.createAgentServer = createAgentServer;
|
|
33
|
+
const secrets_js_1 = require("./agent-definitions/secrets.js");
|
|
34
|
+
const jwt_js_1 = require("./jwt.js");
|
|
35
|
+
const jwt_js_2 = require("./jwt.js");
|
|
36
|
+
const oidc_signin_js_1 = require("./oidc-signin.js");
|
|
37
|
+
// ============================================
|
|
38
|
+
// HTTP Helpers
|
|
39
|
+
// ============================================
|
|
40
|
+
function jsonResponse(data, status = 200) {
|
|
41
|
+
return new Response(JSON.stringify(data), {
|
|
42
|
+
status,
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function corsHeaders() {
|
|
47
|
+
return {
|
|
48
|
+
"Access-Control-Allow-Origin": "*",
|
|
49
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
50
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Actor-Type",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function addCors(res) {
|
|
54
|
+
const headers = new Headers(res.headers);
|
|
55
|
+
for (const [k, v] of Object.entries(corsHeaders())) {
|
|
56
|
+
headers.set(k, v);
|
|
57
|
+
}
|
|
58
|
+
return new Response(res.body, { status: res.status, headers });
|
|
59
|
+
}
|
|
60
|
+
// ============================================
|
|
61
|
+
// JSON-RPC Helpers
|
|
62
|
+
// ============================================
|
|
63
|
+
function jsonRpcSuccess(id, result) {
|
|
64
|
+
return { jsonrpc: "2.0", id, result };
|
|
65
|
+
}
|
|
66
|
+
function jsonRpcError(id, code, message, data) {
|
|
67
|
+
return {
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
id,
|
|
70
|
+
error: { code, message, ...(data !== undefined && { data }) },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Wrap a value as MCP tool result content */
|
|
74
|
+
function mcpResult(value, isError = false) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
...(isError && { isError: true }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ============================================
|
|
86
|
+
// Auth Detection & Resolution
|
|
87
|
+
// ============================================
|
|
88
|
+
function detectAuth(registry) {
|
|
89
|
+
const authAgent = registry.get("@auth");
|
|
90
|
+
if (!authAgent?.__authStore || !authAgent.__rootKey)
|
|
91
|
+
return {};
|
|
92
|
+
return {
|
|
93
|
+
store: authAgent.__authStore,
|
|
94
|
+
rootKey: authAgent.__rootKey,
|
|
95
|
+
tokenTtl: authAgent.__tokenTtl ?? 3600,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function resolveAuth(req, authConfig, jwksOptions) {
|
|
99
|
+
const authHeader = req.headers.get("Authorization");
|
|
100
|
+
if (!authHeader)
|
|
101
|
+
return null;
|
|
102
|
+
const [scheme, credential] = authHeader.split(" ", 2);
|
|
103
|
+
if (scheme?.toLowerCase() !== "bearer" || !credential)
|
|
104
|
+
return null;
|
|
105
|
+
// Root key check
|
|
106
|
+
if (authConfig.rootKey && credential === authConfig.rootKey) {
|
|
107
|
+
return {
|
|
108
|
+
callerId: "root",
|
|
109
|
+
callerType: "system",
|
|
110
|
+
scopes: ["*"],
|
|
111
|
+
isRoot: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Try ES256 verification against own signing keys
|
|
115
|
+
const parts = credential.split(".");
|
|
116
|
+
if (parts.length === 3 && jwksOptions?.signingKeys?.length) {
|
|
117
|
+
for (const key of jwksOptions.signingKeys) {
|
|
118
|
+
try {
|
|
119
|
+
const verified = await (0, jwt_js_2.verifyJwtLocal)(credential, key.publicKey);
|
|
120
|
+
if (verified) {
|
|
121
|
+
return {
|
|
122
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
123
|
+
callerType: "agent",
|
|
124
|
+
scopes: verified.scopes ?? ["*"],
|
|
125
|
+
isRoot: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Try trusted issuers (remote JWKS verification)
|
|
133
|
+
// Trusted issuer verification: decode iss claim, look up in config, verify JWKS
|
|
134
|
+
if (parts.length === 3 && jwksOptions?.trustedIssuers?.length) {
|
|
135
|
+
try {
|
|
136
|
+
// Peek at unverified payload to read iss
|
|
137
|
+
const payloadB64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
138
|
+
const unverified = JSON.parse(atob(payloadB64));
|
|
139
|
+
if (unverified.iss) {
|
|
140
|
+
const issuerConfig = jwksOptions.trustedIssuers.find((i) => i.issuer === unverified.iss);
|
|
141
|
+
if (issuerConfig) {
|
|
142
|
+
const verified = await (0, jwt_js_2.verifyJwtFromIssuer)(credential, issuerConfig.issuer);
|
|
143
|
+
if (verified) {
|
|
144
|
+
const scopes = issuerConfig.scopes;
|
|
145
|
+
const isSystem = scopes.includes("*") || scopes.includes("agents:admin");
|
|
146
|
+
return {
|
|
147
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
148
|
+
callerType: isSystem ? "system" : "agent",
|
|
149
|
+
scopes,
|
|
150
|
+
isRoot: isSystem,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Failed to decode/verify, fall through
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Try HMAC JWT verification (legacy, stateless)
|
|
161
|
+
if (parts.length === 3) {
|
|
162
|
+
try {
|
|
163
|
+
const payloadB64 = parts[1];
|
|
164
|
+
const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
|
165
|
+
const payload = JSON.parse(atob(padded));
|
|
166
|
+
if (payload.sub && authConfig.store) {
|
|
167
|
+
const client = await authConfig.store.getClient(payload.sub);
|
|
168
|
+
if (client) {
|
|
169
|
+
const verified = await (0, jwt_js_1.verifyJwt)(credential, client.clientSecretHash);
|
|
170
|
+
if (verified) {
|
|
171
|
+
return {
|
|
172
|
+
callerId: verified.name || client.name,
|
|
173
|
+
callerType: "agent",
|
|
174
|
+
scopes: verified.scopes,
|
|
175
|
+
isRoot: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Not a valid JWT, fall through to legacy token validation
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Legacy: opaque token validation (backwards compat)
|
|
186
|
+
if (!authConfig.store)
|
|
187
|
+
return null;
|
|
188
|
+
const token = await authConfig.store.validateToken(credential);
|
|
189
|
+
if (!token)
|
|
190
|
+
return null;
|
|
191
|
+
const client = await authConfig.store.getClient(token.clientId);
|
|
192
|
+
return {
|
|
193
|
+
callerId: client?.name ?? token.clientId,
|
|
194
|
+
callerType: "agent",
|
|
195
|
+
scopes: token.scopes,
|
|
196
|
+
isRoot: false,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function canSeeAgent(agent, auth) {
|
|
200
|
+
const visibility = (agent.visibility ??
|
|
201
|
+
agent.config?.visibility ??
|
|
202
|
+
"internal");
|
|
203
|
+
if (auth?.isRoot)
|
|
204
|
+
return true;
|
|
205
|
+
if (visibility === "public")
|
|
206
|
+
return true;
|
|
207
|
+
if (visibility === "internal" && auth)
|
|
208
|
+
return true;
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Filter tools visible on a public agent endpoint.
|
|
213
|
+
* For /agents/ routes, tools inherit the agent's visibility:
|
|
214
|
+
* - If agent is public, tools without explicit visibility are shown
|
|
215
|
+
* - Tool-level visibility still overrides (e.g. visibility: "private" hides it)
|
|
216
|
+
*/
|
|
217
|
+
function getVisibleTools(agent, auth) {
|
|
218
|
+
const agentVisibility = (agent.visibility ??
|
|
219
|
+
agent.config?.visibility ??
|
|
220
|
+
"internal");
|
|
221
|
+
return agent.tools.filter((t) => {
|
|
222
|
+
const tv = t.visibility;
|
|
223
|
+
if (auth?.isRoot)
|
|
224
|
+
return true;
|
|
225
|
+
// Tool has explicit visibility — respect it
|
|
226
|
+
if (tv === "public")
|
|
227
|
+
return true;
|
|
228
|
+
if (tv === "private")
|
|
229
|
+
return auth?.isRoot ?? false;
|
|
230
|
+
if (tv === "internal" && auth)
|
|
231
|
+
return true;
|
|
232
|
+
// No explicit tool visibility — inherit from agent
|
|
233
|
+
if (!tv && agentVisibility === "public")
|
|
234
|
+
return true;
|
|
235
|
+
if (!tv && agentVisibility === "internal" && auth)
|
|
236
|
+
return true;
|
|
237
|
+
return false;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// ============================================
|
|
241
|
+
// MCP Tool Definitions
|
|
242
|
+
// ============================================
|
|
243
|
+
function getToolDefinitions() {
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
name: "call_agent",
|
|
247
|
+
description: "Execute a tool on a registered agent. Provide the agent path and tool name.",
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
request: {
|
|
252
|
+
type: "object",
|
|
253
|
+
description: "The call request",
|
|
254
|
+
properties: {
|
|
255
|
+
action: {
|
|
256
|
+
type: "string",
|
|
257
|
+
enum: ["execute_tool", "describe_tools", "load"],
|
|
258
|
+
description: "Action to perform",
|
|
259
|
+
},
|
|
260
|
+
path: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "Agent path (e.g., '@my-agent')",
|
|
263
|
+
},
|
|
264
|
+
tool: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Tool name to call",
|
|
267
|
+
},
|
|
268
|
+
params: {
|
|
269
|
+
type: "object",
|
|
270
|
+
description: "Parameters for the tool",
|
|
271
|
+
additionalProperties: true,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
required: ["action", "path"],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
required: ["request"],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "list_agents",
|
|
282
|
+
description: "List all registered agents and their available tools.",
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: "object",
|
|
285
|
+
properties: {},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
// ============================================
|
|
291
|
+
// Create Server
|
|
292
|
+
// ============================================
|
|
293
|
+
function createAgentServer(registry, options = {}) {
|
|
294
|
+
const { port = 3000, hostname = "localhost", basePath = "", cors = true, serverName = "agents-sdk", serverVersion = "1.0.0", secretStore, oauthIdentityProvider, } = options;
|
|
295
|
+
// OIDC sign-in handler (if configured)
|
|
296
|
+
const oidcSignIn = options.oidcProvider
|
|
297
|
+
? (0, oidc_signin_js_1.createOIDCSignIn)(options.oidcProvider)
|
|
298
|
+
: null;
|
|
299
|
+
// Signing keys for JWKS-based auth
|
|
300
|
+
const serverSigningKeys = [];
|
|
301
|
+
// Normalize trustedIssuers to TrustedIssuer objects
|
|
302
|
+
const configTrustedIssuers = (options.trustedIssuers ?? []).map((i) => (typeof i === "string" ? { issuer: i, scopes: ["*"] } : i));
|
|
303
|
+
const authConfig = detectAuth(registry);
|
|
304
|
+
let serverInstance = null;
|
|
305
|
+
// ──────────────────────────────────────────
|
|
306
|
+
// JSON-RPC handler
|
|
307
|
+
// ──────────────────────────────────────────
|
|
308
|
+
async function handleJsonRpc(request, auth) {
|
|
309
|
+
switch (request.method) {
|
|
310
|
+
case "initialize":
|
|
311
|
+
return jsonRpcSuccess(request.id, {
|
|
312
|
+
protocolVersion: "2024-11-05",
|
|
313
|
+
capabilities: { tools: { listChanged: false } },
|
|
314
|
+
serverInfo: { name: serverName, version: serverVersion },
|
|
315
|
+
});
|
|
316
|
+
case "notifications/initialized":
|
|
317
|
+
return jsonRpcSuccess(request.id, {});
|
|
318
|
+
case "tools/list":
|
|
319
|
+
return jsonRpcSuccess(request.id, {
|
|
320
|
+
tools: getToolDefinitions(),
|
|
321
|
+
});
|
|
322
|
+
case "tools/call": {
|
|
323
|
+
const { name, arguments: args } = (request.params ?? {});
|
|
324
|
+
try {
|
|
325
|
+
const result = await handleToolCall(name, args ?? {}, auth);
|
|
326
|
+
return jsonRpcSuccess(request.id, result);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
console.error("[server] Tool call error:", err);
|
|
330
|
+
return jsonRpcSuccess(request.id, mcpResult(`Error: ${err instanceof Error ? err.message : String(err)}`, true));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
default:
|
|
334
|
+
return jsonRpcError(request.id, -32601, `Method not found: ${request.method}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// ──────────────────────────────────────────
|
|
338
|
+
// MCP tool implementations
|
|
339
|
+
// ──────────────────────────────────────────
|
|
340
|
+
async function handleToolCall(toolName, args, auth) {
|
|
341
|
+
switch (toolName) {
|
|
342
|
+
case "call_agent": {
|
|
343
|
+
const trigger = args.trigger;
|
|
344
|
+
const req = (args.request ?? args);
|
|
345
|
+
// Inject auth context
|
|
346
|
+
if (auth) {
|
|
347
|
+
req.callerId = auth.callerId;
|
|
348
|
+
req.callerType = auth.callerType;
|
|
349
|
+
if (!req.metadata)
|
|
350
|
+
req.metadata = {};
|
|
351
|
+
req.metadata.scopes = auth.scopes;
|
|
352
|
+
req.metadata.isRoot = auth.isRoot;
|
|
353
|
+
if (auth.issuer)
|
|
354
|
+
req.metadata.issuer = auth.issuer;
|
|
355
|
+
}
|
|
356
|
+
if (auth?.isRoot) {
|
|
357
|
+
req.callerType = "system";
|
|
358
|
+
}
|
|
359
|
+
// Pass trigger through to registry.call() which handles
|
|
360
|
+
// deferred execution via callbackStore if configured.
|
|
361
|
+
if (trigger) {
|
|
362
|
+
req.trigger = trigger;
|
|
363
|
+
}
|
|
364
|
+
// Process secret params: resolve refs, store raw secrets
|
|
365
|
+
if (req.params && secretStore) {
|
|
366
|
+
const ownerId = auth?.callerId ?? "anonymous";
|
|
367
|
+
const agent = registry.get(req.path);
|
|
368
|
+
const tool = agent?.tools.find((t) => t.name === req.tool);
|
|
369
|
+
const schema = tool?.inputSchema;
|
|
370
|
+
const { resolved } = await (0, secrets_js_1.processSecretParams)(req.params, schema, secretStore, ownerId);
|
|
371
|
+
req.params = resolved;
|
|
372
|
+
}
|
|
373
|
+
const result = await registry.call(req);
|
|
374
|
+
return mcpResult(result);
|
|
375
|
+
}
|
|
376
|
+
case "list_agents": {
|
|
377
|
+
const agents = registry.list();
|
|
378
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
379
|
+
return mcpResult({
|
|
380
|
+
success: true,
|
|
381
|
+
agents: visible.map((agent) => ({
|
|
382
|
+
path: agent.path,
|
|
383
|
+
name: agent.config?.name,
|
|
384
|
+
description: agent.config?.description,
|
|
385
|
+
supportedActions: agent.config?.supportedActions,
|
|
386
|
+
integration: agent.config?.integration || null,
|
|
387
|
+
tools: agent.tools
|
|
388
|
+
.filter((t) => {
|
|
389
|
+
const tv = t.visibility ?? "internal";
|
|
390
|
+
if (auth?.isRoot)
|
|
391
|
+
return true;
|
|
392
|
+
if (tv === "public")
|
|
393
|
+
return true;
|
|
394
|
+
if (tv === "authenticated" &&
|
|
395
|
+
auth?.callerId &&
|
|
396
|
+
auth.callerId !== "anonymous")
|
|
397
|
+
return true;
|
|
398
|
+
if (tv === "internal" && auth)
|
|
399
|
+
return true;
|
|
400
|
+
return false;
|
|
401
|
+
})
|
|
402
|
+
.map((t) => t.name),
|
|
403
|
+
})),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
default:
|
|
407
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// ──────────────────────────────────────────
|
|
411
|
+
// OAuth2 token handler
|
|
412
|
+
// ──────────────────────────────────────────
|
|
413
|
+
// Resolve public-facing base URL, respecting reverse proxy headers
|
|
414
|
+
const resolveBaseUrl = (r) => {
|
|
415
|
+
const fwdProto = r.headers.get("x-forwarded-proto");
|
|
416
|
+
const fwdHost = r.headers.get("x-forwarded-host");
|
|
417
|
+
if (fwdProto && fwdHost)
|
|
418
|
+
return `${fwdProto}://${fwdHost}`;
|
|
419
|
+
return new URL(r.url).origin;
|
|
420
|
+
};
|
|
421
|
+
async function handleOAuthToken(req) {
|
|
422
|
+
if (!authConfig) {
|
|
423
|
+
return jsonResponse({ error: "auth_not_configured" }, 404);
|
|
424
|
+
}
|
|
425
|
+
const contentType = req.headers.get("Content-Type") ?? "";
|
|
426
|
+
let params;
|
|
427
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
428
|
+
const body = await req.text();
|
|
429
|
+
const urlParams = new URLSearchParams(body);
|
|
430
|
+
params = Object.fromEntries(urlParams.entries());
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
params = (await req.json());
|
|
434
|
+
}
|
|
435
|
+
const grantType = params.grant_type ?? "";
|
|
436
|
+
// ── jwt_exchange grant: verify foreign JWT, resolve local identity ──
|
|
437
|
+
if (grantType === "jwt_exchange") {
|
|
438
|
+
const assertion = params.assertion ?? "";
|
|
439
|
+
if (!assertion) {
|
|
440
|
+
return jsonResponse({
|
|
441
|
+
error: "invalid_request",
|
|
442
|
+
error_description: "Missing assertion parameter",
|
|
443
|
+
}, 400);
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const result = await registry.call({
|
|
447
|
+
action: "execute_tool",
|
|
448
|
+
path: "@auth",
|
|
449
|
+
tool: "exchange_token",
|
|
450
|
+
params: { token: assertion },
|
|
451
|
+
callerType: "system",
|
|
452
|
+
});
|
|
453
|
+
const exchangeResult = result?.result;
|
|
454
|
+
// If the tool call failed, forward the error
|
|
455
|
+
if (result?.success === false) {
|
|
456
|
+
return jsonResponse({
|
|
457
|
+
error: "server_error",
|
|
458
|
+
error_description: result?.error ?? "Exchange tool failed",
|
|
459
|
+
raw: JSON.stringify(result)?.slice(0, 300),
|
|
460
|
+
}, 500);
|
|
461
|
+
}
|
|
462
|
+
// ── Reverse registration: if caller is an agent-registry, auto-store connection ──
|
|
463
|
+
try {
|
|
464
|
+
const assertionParts = assertion.split(".");
|
|
465
|
+
if (assertionParts.length === 3) {
|
|
466
|
+
const assertionPayload = JSON.parse(Buffer.from(assertionParts[1], "base64url").toString());
|
|
467
|
+
if (assertionPayload.type === "agent-registry" &&
|
|
468
|
+
assertionPayload.iss) {
|
|
469
|
+
// Use add_connection (direct store) instead of setup_integration (which would cause infinite loop)
|
|
470
|
+
const addResult = await registry.call({
|
|
471
|
+
action: "execute_tool",
|
|
472
|
+
path: "@remote-registry",
|
|
473
|
+
tool: "add_connection",
|
|
474
|
+
params: {
|
|
475
|
+
id: assertionPayload.name ?? "remote-registry",
|
|
476
|
+
name: assertionPayload.name ?? "remote-registry",
|
|
477
|
+
url: assertionPayload.iss,
|
|
478
|
+
remoteTenantId: assertionPayload.tenantId ?? "default",
|
|
479
|
+
},
|
|
480
|
+
callerId: "system",
|
|
481
|
+
callerType: "system",
|
|
482
|
+
});
|
|
483
|
+
if (addResult.success) {
|
|
484
|
+
console.error(`[jwt_exchange] Reverse connection stored for ${assertionPayload.iss}`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
console.error("[jwt_exchange] Reverse connection failed:", addResult.error);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (reverseErr) {
|
|
493
|
+
console.error("[jwt_exchange] Reverse registration check failed:", reverseErr);
|
|
494
|
+
}
|
|
495
|
+
if (!exchangeResult) {
|
|
496
|
+
return jsonResponse({
|
|
497
|
+
error: "server_error",
|
|
498
|
+
error_description: `Exchange returned null: ${JSON.stringify(result)?.slice(0, 300)}`,
|
|
499
|
+
}, 500);
|
|
500
|
+
}
|
|
501
|
+
// User not linked yet — needs OAuth identity linking
|
|
502
|
+
if (exchangeResult.needsAuth) {
|
|
503
|
+
const baseUrl = resolveBaseUrl(req);
|
|
504
|
+
const authorizeUrl = new URL(`${baseUrl}${basePath}/oauth/authorize`);
|
|
505
|
+
authorizeUrl.searchParams.set("token", assertion);
|
|
506
|
+
if (params.redirect_uri) {
|
|
507
|
+
authorizeUrl.searchParams.set("redirect_uri", params.redirect_uri);
|
|
508
|
+
}
|
|
509
|
+
if (params.scope) {
|
|
510
|
+
authorizeUrl.searchParams.set("scope", params.scope);
|
|
511
|
+
}
|
|
512
|
+
return jsonResponse({
|
|
513
|
+
error: "identity_required",
|
|
514
|
+
error_description: "User identity not linked. Redirect to authorize_url to complete linking.",
|
|
515
|
+
authorize_url: authorizeUrl.toString(),
|
|
516
|
+
tenant_id: exchangeResult.tenantId,
|
|
517
|
+
}, 403);
|
|
518
|
+
}
|
|
519
|
+
// User found — sign a local access token
|
|
520
|
+
if (exchangeResult.userId && serverSigningKeys.length > 0) {
|
|
521
|
+
const sigKey = serverSigningKeys[0];
|
|
522
|
+
const token = await (0, jwt_js_2.signJwtES256)({
|
|
523
|
+
sub: exchangeResult.userId,
|
|
524
|
+
name: exchangeResult.userId,
|
|
525
|
+
scopes: ["*"],
|
|
526
|
+
tenantId: exchangeResult.tenantId,
|
|
527
|
+
}, sigKey.privateKey, sigKey.kid, resolveBaseUrl(req), `${authConfig.tokenTtl ?? 3600}s`);
|
|
528
|
+
return jsonResponse({
|
|
529
|
+
access_token: token,
|
|
530
|
+
token_type: "Bearer",
|
|
531
|
+
expires_in: authConfig.tokenTtl ?? 3600,
|
|
532
|
+
user_id: exchangeResult.userId,
|
|
533
|
+
tenant_id: exchangeResult.tenantId,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return jsonResponse(exchangeResult);
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
console.error("[oauth] JWT exchange error:", err);
|
|
540
|
+
return jsonResponse({
|
|
541
|
+
error: "server_error",
|
|
542
|
+
error_description: `JWT exchange failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
543
|
+
}, 500);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// ── client_credentials grant ──
|
|
547
|
+
if (grantType === "client_credentials") {
|
|
548
|
+
const clientId = params.client_id ?? "";
|
|
549
|
+
const clientSecret = params.client_secret ?? "";
|
|
550
|
+
if (!clientId || !clientSecret) {
|
|
551
|
+
return jsonResponse({
|
|
552
|
+
error: "invalid_request",
|
|
553
|
+
error_description: "Missing client_id or client_secret",
|
|
554
|
+
}, 400);
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const result = await registry.call({
|
|
558
|
+
action: "execute_tool",
|
|
559
|
+
path: "@auth",
|
|
560
|
+
tool: "token",
|
|
561
|
+
params: { clientId, clientSecret },
|
|
562
|
+
callerType: "system",
|
|
563
|
+
});
|
|
564
|
+
const tokenResult = result?.result;
|
|
565
|
+
if (!tokenResult?.accessToken) {
|
|
566
|
+
return jsonResponse({
|
|
567
|
+
error: "invalid_client",
|
|
568
|
+
error_description: "Authentication failed",
|
|
569
|
+
}, 401);
|
|
570
|
+
}
|
|
571
|
+
return jsonResponse({
|
|
572
|
+
access_token: tokenResult.accessToken,
|
|
573
|
+
token_type: "Bearer",
|
|
574
|
+
expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
|
|
575
|
+
refresh_token: tokenResult.refreshToken,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
console.error("[oauth] Token error:", err);
|
|
580
|
+
return jsonResponse({ error: "server_error", error_description: "Token exchange failed" }, 500);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return jsonResponse({
|
|
584
|
+
error: "unsupported_grant_type",
|
|
585
|
+
error_description: "Supported grant types: client_credentials, jwt_exchange",
|
|
586
|
+
}, 400);
|
|
587
|
+
}
|
|
588
|
+
// ──────────────────────────────────────────
|
|
589
|
+
// Main fetch handler
|
|
590
|
+
// ──────────────────────────────────────────
|
|
591
|
+
async function fetch(req) {
|
|
592
|
+
try {
|
|
593
|
+
const url = new URL(req.url);
|
|
594
|
+
const path = url.pathname.replace(basePath, "") || "/";
|
|
595
|
+
// CORS preflight
|
|
596
|
+
if (cors && req.method === "OPTIONS") {
|
|
597
|
+
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
598
|
+
}
|
|
599
|
+
// Resolve auth for all requests
|
|
600
|
+
const auth = await resolveAuth(req, authConfig, {
|
|
601
|
+
signingKeys: serverSigningKeys,
|
|
602
|
+
trustedIssuers: configTrustedIssuers,
|
|
603
|
+
});
|
|
604
|
+
// Also check header-based identity (for proxied requests)
|
|
605
|
+
const headerAuth = !auth
|
|
606
|
+
? (() => {
|
|
607
|
+
const actorId = req.headers.get("X-Atlas-Actor-Id");
|
|
608
|
+
const actorType = req.headers.get("X-Atlas-Actor-Type");
|
|
609
|
+
if (actorId) {
|
|
610
|
+
return {
|
|
611
|
+
callerId: actorId,
|
|
612
|
+
callerType: actorType ?? "agent",
|
|
613
|
+
scopes: ["*"],
|
|
614
|
+
isRoot: false,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
})()
|
|
619
|
+
: null;
|
|
620
|
+
const effectiveAuth = auth ?? headerAuth;
|
|
621
|
+
// ── POST / → MCP JSON-RPC ──
|
|
622
|
+
if (path === "/" && req.method === "POST") {
|
|
623
|
+
const body = (await req.json());
|
|
624
|
+
const result = await handleJsonRpc(body, effectiveAuth);
|
|
625
|
+
return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
|
|
626
|
+
}
|
|
627
|
+
// ── POST /oauth/token → OAuth2 token exchange ──
|
|
628
|
+
if (path === "/oauth/token" && req.method === "POST") {
|
|
629
|
+
const res = await handleOAuthToken(req);
|
|
630
|
+
return cors ? addCors(res) : res;
|
|
631
|
+
}
|
|
632
|
+
// ── OIDC Sign-In (authorize + callback) ──
|
|
633
|
+
if (oidcSignIn &&
|
|
634
|
+
(path === "/signin/authorize" || path === "/signin/callback")) {
|
|
635
|
+
const baseUrl = resolveBaseUrl(req);
|
|
636
|
+
const res = await oidcSignIn.handleRequest(req, {
|
|
637
|
+
baseUrl: baseUrl + basePath,
|
|
638
|
+
signingKey: serverSigningKeys[0],
|
|
639
|
+
issuerUrl: baseUrl,
|
|
640
|
+
});
|
|
641
|
+
if (res)
|
|
642
|
+
return cors ? addCors(res) : res;
|
|
643
|
+
}
|
|
644
|
+
// ── GET /oauth/authorize → Identity linking redirect (browser flow) ──
|
|
645
|
+
if (path === "/oauth/authorize" && req.method === "GET") {
|
|
646
|
+
if (!oauthIdentityProvider) {
|
|
647
|
+
const res = jsonResponse({
|
|
648
|
+
error: "not_configured",
|
|
649
|
+
error_description: "No OAuth identity provider configured",
|
|
650
|
+
}, 404);
|
|
651
|
+
return cors ? addCors(res) : res;
|
|
652
|
+
}
|
|
653
|
+
const url = new URL(req.url);
|
|
654
|
+
const token = url.searchParams.get("token") ?? "";
|
|
655
|
+
const redirectUri = url.searchParams.get("redirect_uri") ?? "";
|
|
656
|
+
if (!token) {
|
|
657
|
+
const res = jsonResponse({
|
|
658
|
+
error: "invalid_request",
|
|
659
|
+
error_description: "Missing token parameter",
|
|
660
|
+
}, 400);
|
|
661
|
+
return cors ? addCors(res) : res;
|
|
662
|
+
}
|
|
663
|
+
// Verify the JWT against trusted issuers (from store, falling back to config)
|
|
664
|
+
let claims = null;
|
|
665
|
+
const storeIssuers = authConfig?.store
|
|
666
|
+
? await authConfig.store.listTrustedIssuers()
|
|
667
|
+
: [];
|
|
668
|
+
const configIssuerUrls = configTrustedIssuers.map((i) => typeof i === "string" ? i : i.issuer);
|
|
669
|
+
const allIssuerUrls = [
|
|
670
|
+
...new Set([...storeIssuers, ...configIssuerUrls]),
|
|
671
|
+
];
|
|
672
|
+
console.log("[oauth/authorize] storeIssuers:", storeIssuers.length, "configIssuers:", configIssuerUrls.length, "total:", allIssuerUrls.length, "urls:", allIssuerUrls);
|
|
673
|
+
for (const issuerUrl of allIssuerUrls) {
|
|
674
|
+
try {
|
|
675
|
+
const result = await (0, jwt_js_2.verifyJwtFromIssuer)(token, issuerUrl);
|
|
676
|
+
console.log("[oauth/authorize] verify", issuerUrl, "->", result ? "OK" : "null");
|
|
677
|
+
if (result) {
|
|
678
|
+
claims = result;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
console.log("[oauth/authorize] verify", issuerUrl, "-> ERROR:", e.message);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (!claims) {
|
|
687
|
+
const res = jsonResponse({
|
|
688
|
+
error: "invalid_token",
|
|
689
|
+
error_description: "JWT verification failed against all trusted issuers",
|
|
690
|
+
}, 401);
|
|
691
|
+
return cors ? addCors(res) : res;
|
|
692
|
+
}
|
|
693
|
+
const baseUrl = resolveBaseUrl(req);
|
|
694
|
+
const scope = url.searchParams.get("scope") ?? undefined;
|
|
695
|
+
const res = await oauthIdentityProvider.authorize(req, {
|
|
696
|
+
token,
|
|
697
|
+
claims,
|
|
698
|
+
redirectUri,
|
|
699
|
+
baseUrl: baseUrl + basePath,
|
|
700
|
+
scope,
|
|
701
|
+
});
|
|
702
|
+
return cors ? addCors(res) : res;
|
|
703
|
+
}
|
|
704
|
+
// ── GET /oauth/callback → Identity linking callback ──
|
|
705
|
+
if (path === "/oauth/callback" && req.method === "GET") {
|
|
706
|
+
if (!oauthIdentityProvider) {
|
|
707
|
+
const res = jsonResponse({
|
|
708
|
+
error: "not_configured",
|
|
709
|
+
error_description: "No OAuth identity provider configured",
|
|
710
|
+
}, 404);
|
|
711
|
+
return cors ? addCors(res) : res;
|
|
712
|
+
}
|
|
713
|
+
const baseUrl = resolveBaseUrl(req);
|
|
714
|
+
const res = await oauthIdentityProvider.callback(req, {
|
|
715
|
+
baseUrl: baseUrl + basePath,
|
|
716
|
+
});
|
|
717
|
+
return cors ? addCors(res) : res;
|
|
718
|
+
}
|
|
719
|
+
// ── GET /health → Health check ──
|
|
720
|
+
if (path === "/health" && req.method === "GET") {
|
|
721
|
+
const res = jsonResponse({
|
|
722
|
+
status: "ok",
|
|
723
|
+
agents: registry.listPaths(),
|
|
724
|
+
});
|
|
725
|
+
return cors ? addCors(res) : res;
|
|
726
|
+
}
|
|
727
|
+
// ── GET /.well-known/jwks.json → JWKS public keys ──
|
|
728
|
+
if (path === "/.well-known/jwks.json" && req.method === "GET") {
|
|
729
|
+
const jwks = serverSigningKeys.length > 0
|
|
730
|
+
? await (0, jwt_js_2.buildJwks)(serverSigningKeys)
|
|
731
|
+
: { keys: [] };
|
|
732
|
+
const res = jsonResponse(jwks);
|
|
733
|
+
return cors ? addCors(res) : res;
|
|
734
|
+
}
|
|
735
|
+
// ── GET /.well-known/configuration → Server discovery ──
|
|
736
|
+
if (path === "/.well-known/configuration" && req.method === "GET") {
|
|
737
|
+
const baseUrl = resolveBaseUrl(req);
|
|
738
|
+
const res = jsonResponse({
|
|
739
|
+
issuer: baseUrl,
|
|
740
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
741
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
742
|
+
agents_endpoint: `${baseUrl}/list`,
|
|
743
|
+
call_endpoint: baseUrl,
|
|
744
|
+
supported_grant_types: ["client_credentials", "jwt_exchange"],
|
|
745
|
+
authorization_endpoint: `${baseUrl}/oauth/authorize`,
|
|
746
|
+
...(oidcSignIn
|
|
747
|
+
? { signin_endpoint: `${baseUrl}/signin/authorize` }
|
|
748
|
+
: {}),
|
|
749
|
+
});
|
|
750
|
+
return cors ? addCors(res) : res;
|
|
751
|
+
}
|
|
752
|
+
// ── GET /list → List agents (legacy endpoint) ──
|
|
753
|
+
if (path === "/list" && req.method === "GET") {
|
|
754
|
+
const agents = registry.list();
|
|
755
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
|
|
756
|
+
const res = jsonResponse(visible.map((agent) => ({
|
|
757
|
+
path: agent.path,
|
|
758
|
+
name: agent.config?.name,
|
|
759
|
+
description: agent.config?.description,
|
|
760
|
+
supportedActions: agent.config?.supportedActions,
|
|
761
|
+
integration: agent.config?.integration || null,
|
|
762
|
+
tools: agent.tools
|
|
763
|
+
.filter((t) => {
|
|
764
|
+
const tv = t.visibility ?? "internal";
|
|
765
|
+
if (effectiveAuth?.isRoot)
|
|
766
|
+
return true;
|
|
767
|
+
if (tv === "public")
|
|
768
|
+
return true;
|
|
769
|
+
if (tv === "internal" && effectiveAuth)
|
|
770
|
+
return true;
|
|
771
|
+
return false;
|
|
772
|
+
})
|
|
773
|
+
.map((t) => ({
|
|
774
|
+
name: t.name,
|
|
775
|
+
description: t.description,
|
|
776
|
+
})),
|
|
777
|
+
})));
|
|
778
|
+
return cors ? addCors(res) : res;
|
|
779
|
+
}
|
|
780
|
+
// ── GET /agents → List public agents (discovery endpoint) ──
|
|
781
|
+
if (path === "/agents" && req.method === "GET") {
|
|
782
|
+
const agents = registry.list();
|
|
783
|
+
const visible = agents.filter((agent) => {
|
|
784
|
+
// Only show agents with explicit visibility
|
|
785
|
+
if (!agent.visibility)
|
|
786
|
+
return false;
|
|
787
|
+
return canSeeAgent(agent, effectiveAuth);
|
|
788
|
+
});
|
|
789
|
+
const res = jsonResponse(visible.map((agent) => ({
|
|
790
|
+
path: agent.path,
|
|
791
|
+
name: agent.config?.name,
|
|
792
|
+
description: agent.config?.description ?? agent.entrypoint?.slice(0, 200),
|
|
793
|
+
tools: getVisibleTools(agent, effectiveAuth).map((t) => ({
|
|
794
|
+
name: t.name,
|
|
795
|
+
description: t.description,
|
|
796
|
+
})),
|
|
797
|
+
})));
|
|
798
|
+
return cors ? addCors(res) : res;
|
|
799
|
+
}
|
|
800
|
+
// ── GET /agents/{name} → Agent info (single agent discovery) ──
|
|
801
|
+
if (path.startsWith("/agents/") && req.method === "GET") {
|
|
802
|
+
const agentPath = path.slice("/agents/".length); // e.g. "notion"
|
|
803
|
+
const agent = registry.get(agentPath) ?? registry.get(`@${agentPath}`);
|
|
804
|
+
if (!agent || !canSeeAgent(agent, effectiveAuth)) {
|
|
805
|
+
const res = jsonResponse({ error: "not_found", message: `Agent not found: ${agentPath}` }, 404);
|
|
806
|
+
return cors ? addCors(res) : res;
|
|
807
|
+
}
|
|
808
|
+
const res = jsonResponse({
|
|
809
|
+
path: agent.path,
|
|
810
|
+
name: agent.config?.name,
|
|
811
|
+
description: agent.config?.description ?? agent.entrypoint?.slice(0, 200),
|
|
812
|
+
tools: getVisibleTools(agent, effectiveAuth).map((t) => ({
|
|
813
|
+
name: t.name,
|
|
814
|
+
description: t.description,
|
|
815
|
+
inputSchema: t.inputSchema,
|
|
816
|
+
})),
|
|
817
|
+
});
|
|
818
|
+
return cors ? addCors(res) : res;
|
|
819
|
+
}
|
|
820
|
+
// ── POST /agents/{name} → Scoped MCP call to single agent ──
|
|
821
|
+
if (path.startsWith("/agents/") && req.method === "POST") {
|
|
822
|
+
const agentPath = path.slice("/agents/".length);
|
|
823
|
+
const agent = registry.get(agentPath) ?? registry.get(`@${agentPath}`);
|
|
824
|
+
if (!agent || !canSeeAgent(agent, effectiveAuth)) {
|
|
825
|
+
const res = jsonResponse({ error: "not_found", message: `Agent not found: ${agentPath}` }, 404);
|
|
826
|
+
return cors ? addCors(res) : res;
|
|
827
|
+
}
|
|
828
|
+
const body = (await req.json());
|
|
829
|
+
// Scoped JSON-RPC: tools/list only shows this agent's tools
|
|
830
|
+
if (body.method === "initialize") {
|
|
831
|
+
const res = jsonResponse(jsonRpcSuccess(body.id, {
|
|
832
|
+
protocolVersion: "2024-11-05",
|
|
833
|
+
capabilities: { tools: { listChanged: false } },
|
|
834
|
+
serverInfo: {
|
|
835
|
+
name: agent.config?.name ?? agent.path,
|
|
836
|
+
version: "1.0.0",
|
|
837
|
+
},
|
|
838
|
+
}));
|
|
839
|
+
return cors ? addCors(res) : res;
|
|
840
|
+
}
|
|
841
|
+
if (body.method === "tools/list") {
|
|
842
|
+
const tools = getVisibleTools(agent, effectiveAuth).map((t) => ({
|
|
843
|
+
name: t.name,
|
|
844
|
+
description: t.description,
|
|
845
|
+
inputSchema: t.inputSchema,
|
|
846
|
+
}));
|
|
847
|
+
const res = jsonResponse(jsonRpcSuccess(body.id, { tools }));
|
|
848
|
+
return cors ? addCors(res) : res;
|
|
849
|
+
}
|
|
850
|
+
if (body.method === "tools/call") {
|
|
851
|
+
// Auth required for tool execution
|
|
852
|
+
if (!effectiveAuth) {
|
|
853
|
+
const res = jsonResponse(jsonRpcError(body.id, -32600, "Authentication required to call tools"), 401);
|
|
854
|
+
return cors ? addCors(res) : res;
|
|
855
|
+
}
|
|
856
|
+
const { name, arguments: args } = (body.params ?? {});
|
|
857
|
+
try {
|
|
858
|
+
const result = await registry.call({
|
|
859
|
+
action: "execute_tool",
|
|
860
|
+
path: agent.path,
|
|
861
|
+
tool: name,
|
|
862
|
+
params: args ?? {},
|
|
863
|
+
callerId: effectiveAuth?.callerId,
|
|
864
|
+
callerType: effectiveAuth?.callerType ?? "external",
|
|
865
|
+
metadata: effectiveAuth
|
|
866
|
+
? {
|
|
867
|
+
scopes: effectiveAuth.scopes,
|
|
868
|
+
isRoot: effectiveAuth.isRoot,
|
|
869
|
+
...(effectiveAuth.issuer
|
|
870
|
+
? { issuer: effectiveAuth.issuer }
|
|
871
|
+
: {}),
|
|
872
|
+
}
|
|
873
|
+
: undefined,
|
|
874
|
+
});
|
|
875
|
+
const res = jsonResponse(jsonRpcSuccess(body.id, result));
|
|
876
|
+
return cors ? addCors(res) : res;
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
console.error("[server] Scoped tool call error:", err);
|
|
880
|
+
const res = jsonResponse(jsonRpcSuccess(body.id, mcpResult(`Error: ${err instanceof Error ? err.message : String(err)}`, true)));
|
|
881
|
+
return cors ? addCors(res) : res;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const res = jsonResponse(jsonRpcError(body.id, -32601, `Method not found: ${body.method}`));
|
|
885
|
+
return cors ? addCors(res) : res;
|
|
886
|
+
}
|
|
887
|
+
// ── Not found ──
|
|
888
|
+
const res = jsonResponse({
|
|
889
|
+
jsonrpc: "2.0",
|
|
890
|
+
id: null,
|
|
891
|
+
error: {
|
|
892
|
+
code: -32601,
|
|
893
|
+
message: `Not found: ${req.method} ${path}`,
|
|
894
|
+
},
|
|
895
|
+
}, 404);
|
|
896
|
+
return cors ? addCors(res) : res;
|
|
897
|
+
}
|
|
898
|
+
catch (err) {
|
|
899
|
+
console.error("[server] Request error:", err);
|
|
900
|
+
const res = jsonResponse({
|
|
901
|
+
jsonrpc: "2.0",
|
|
902
|
+
id: null,
|
|
903
|
+
error: {
|
|
904
|
+
code: -32603,
|
|
905
|
+
message: err instanceof Error ? err.message : "Internal error",
|
|
906
|
+
},
|
|
907
|
+
}, 500);
|
|
908
|
+
return cors ? addCors(res) : res;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// ──────────────────────────────────────────
|
|
912
|
+
// Server lifecycle
|
|
913
|
+
// ──────────────────────────────────────────
|
|
914
|
+
return {
|
|
915
|
+
url: null,
|
|
916
|
+
registry,
|
|
917
|
+
async initKeys() {
|
|
918
|
+
// Load or generate signing keys (without starting Bun.serve)
|
|
919
|
+
if (options.signingKey && serverSigningKeys.length === 0) {
|
|
920
|
+
serverSigningKeys.push(options.signingKey);
|
|
921
|
+
}
|
|
922
|
+
else if (authConfig?.store && serverSigningKeys.length === 0) {
|
|
923
|
+
const stored = (await authConfig.store.getSigningKeys()) ?? [];
|
|
924
|
+
for (const exported of stored) {
|
|
925
|
+
serverSigningKeys.push(await (0, jwt_js_2.importSigningKey)(exported));
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (serverSigningKeys.length === 0) {
|
|
929
|
+
const key = await (0, jwt_js_2.generateSigningKey)();
|
|
930
|
+
serverSigningKeys.push(key);
|
|
931
|
+
if (authConfig?.store) {
|
|
932
|
+
await authConfig.store.storeSigningKey(await (0, jwt_js_2.exportSigningKey)(key));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
async start() {
|
|
937
|
+
await this.initKeys();
|
|
938
|
+
serverInstance = Bun.serve({
|
|
939
|
+
port,
|
|
940
|
+
hostname,
|
|
941
|
+
fetch,
|
|
942
|
+
});
|
|
943
|
+
this.url = `http://${hostname}:${port}`;
|
|
944
|
+
console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
|
|
945
|
+
},
|
|
946
|
+
async stop() {
|
|
947
|
+
if (serverInstance) {
|
|
948
|
+
serverInstance.stop();
|
|
949
|
+
serverInstance = null;
|
|
950
|
+
this.url = null;
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
fetch,
|
|
954
|
+
async signJwt(claims) {
|
|
955
|
+
if (serverSigningKeys.length === 0) {
|
|
956
|
+
throw new Error("No signing keys available. Call start() or initKeys() first.");
|
|
957
|
+
}
|
|
958
|
+
const key = serverSigningKeys[0];
|
|
959
|
+
return (0, jwt_js_2.signJwtES256)({ sub: "system", name: "atlas-os", scopes: ["*"], ...claims }, key.privateKey, key.kid, options.serverName ?? "agents-sdk", "1h");
|
|
960
|
+
},
|
|
961
|
+
addTrustedIssuer(issuerUrl, scopes) {
|
|
962
|
+
// Avoid duplicates
|
|
963
|
+
const existing = configTrustedIssuers.find((i) => i.issuer === issuerUrl);
|
|
964
|
+
if (!existing) {
|
|
965
|
+
configTrustedIssuers.push({
|
|
966
|
+
issuer: issuerUrl,
|
|
967
|
+
scopes: scopes ?? ["*"],
|
|
968
|
+
});
|
|
969
|
+
console.error(`[agent-server] Added trusted issuer: ${issuerUrl}`);
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
//# sourceMappingURL=server.js.map
|