@slashfi/agents-sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.d.ts +20 -33
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +256 -162
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/server.ts +343 -226
package/src/server.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent Server
|
|
2
|
+
* Agent Server (MCP over HTTP)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Compatible with
|
|
4
|
+
* JSON-RPC server implementing the MCP protocol for agent interaction.
|
|
5
|
+
* Compatible with atlas-environments and any MCP client.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
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
|
|
11
|
+
*
|
|
12
|
+
* MCP Tools exposed:
|
|
13
|
+
* - call_agent → Execute a tool on a registered agent
|
|
14
|
+
* - list_agents → List registered agents and their tools
|
|
15
|
+
*
|
|
16
|
+
* Additional endpoints:
|
|
17
|
+
* - POST /oauth/token → OAuth2 client_credentials (when @auth registered)
|
|
18
|
+
* - GET /health → Health check
|
|
11
19
|
*
|
|
12
20
|
* Auth Integration:
|
|
13
21
|
* When an `@auth` agent is registered, the server automatically:
|
|
14
22
|
* - Validates Bearer tokens on requests
|
|
15
23
|
* - Resolves tokens to identity + scopes
|
|
16
|
-
* - Populates
|
|
24
|
+
* - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
|
|
17
25
|
* - Recognizes the root key for admin access
|
|
18
|
-
* - Mounts the /oauth/token endpoint
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
import type { AuthStore } from "./auth.js";
|
|
@@ -26,45 +33,52 @@ import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
|
|
|
26
33
|
// Server Types
|
|
27
34
|
// ============================================
|
|
28
35
|
|
|
29
|
-
/**
|
|
30
|
-
* Server configuration options.
|
|
31
|
-
*/
|
|
32
36
|
export interface AgentServerOptions {
|
|
33
37
|
/** Port to listen on (default: 3000) */
|
|
34
38
|
port?: number;
|
|
35
|
-
|
|
36
39
|
/** Hostname to bind to (default: 'localhost') */
|
|
37
40
|
hostname?: string;
|
|
38
|
-
|
|
39
41
|
/** Base path for endpoints (default: '') */
|
|
40
42
|
basePath?: string;
|
|
41
|
-
|
|
42
43
|
/** Enable CORS (default: true) */
|
|
43
44
|
cors?: boolean;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/** Server name reported in MCP initialize (default: 'agents-sdk') */
|
|
46
|
+
serverName?: string;
|
|
47
|
+
/** Server version reported in MCP initialize (default: '1.0.0') */
|
|
48
|
+
serverVersion?: string;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
/**
|
|
50
|
-
* Agent server instance.
|
|
51
|
-
*/
|
|
52
51
|
export interface AgentServer {
|
|
53
52
|
/** Start the server */
|
|
54
53
|
start(): Promise<void>;
|
|
55
|
-
|
|
56
54
|
/** Stop the server */
|
|
57
55
|
stop(): Promise<void>;
|
|
58
|
-
|
|
59
56
|
/** Handle a request (for custom integrations) */
|
|
60
57
|
fetch(req: Request): Promise<Response>;
|
|
61
|
-
|
|
62
58
|
/** Get the server URL (only available after start) */
|
|
63
59
|
url: string | null;
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
// ============================================
|
|
67
|
-
//
|
|
63
|
+
// JSON-RPC Types
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
interface JsonRpcRequest {
|
|
67
|
+
jsonrpc?: string;
|
|
68
|
+
id?: unknown;
|
|
69
|
+
method: string;
|
|
70
|
+
params?: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface JsonRpcResponse {
|
|
74
|
+
jsonrpc: string;
|
|
75
|
+
id: unknown;
|
|
76
|
+
result?: unknown;
|
|
77
|
+
error?: { code: number; message: string; data?: unknown };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================
|
|
81
|
+
// Auth Types
|
|
68
82
|
// ============================================
|
|
69
83
|
|
|
70
84
|
interface AuthConfig {
|
|
@@ -81,15 +95,13 @@ interface ResolvedAuth {
|
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
// ============================================
|
|
84
|
-
//
|
|
98
|
+
// Helpers
|
|
85
99
|
// ============================================
|
|
86
100
|
|
|
87
101
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
88
102
|
return new Response(JSON.stringify(data), {
|
|
89
103
|
status,
|
|
90
|
-
headers: {
|
|
91
|
-
"Content-Type": "application/json",
|
|
92
|
-
},
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
93
105
|
});
|
|
94
106
|
}
|
|
95
107
|
|
|
@@ -97,7 +109,34 @@ function corsHeaders(): Record<string, string> {
|
|
|
97
109
|
return {
|
|
98
110
|
"Access-Control-Allow-Origin": "*",
|
|
99
111
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
100
|
-
"Access-Control-Allow-Headers":
|
|
112
|
+
"Access-Control-Allow-Headers":
|
|
113
|
+
"Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Agent-Id, X-Atlas-Session-Id",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function jsonRpcSuccess(id: unknown, result: unknown): JsonRpcResponse {
|
|
118
|
+
return { jsonrpc: "2.0", id, result };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function jsonRpcError(
|
|
122
|
+
id: unknown,
|
|
123
|
+
code: number,
|
|
124
|
+
message: string,
|
|
125
|
+
data?: unknown,
|
|
126
|
+
): JsonRpcResponse {
|
|
127
|
+
return { jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined && { data }) } };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Wrap a value as MCP tool result content */
|
|
131
|
+
function mcpResult(value: unknown, isError = false) {
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: "text",
|
|
136
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
...(isError && { isError: true }),
|
|
101
140
|
};
|
|
102
141
|
}
|
|
103
142
|
|
|
@@ -133,23 +172,14 @@ async function resolveAuth(
|
|
|
133
172
|
const [scheme, credential] = authHeader.split(" ", 2);
|
|
134
173
|
if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
|
|
135
174
|
|
|
136
|
-
// Check root key
|
|
137
175
|
if (credential === authConfig.rootKey) {
|
|
138
|
-
return {
|
|
139
|
-
callerId: "root",
|
|
140
|
-
callerType: "system",
|
|
141
|
-
scopes: ["*"],
|
|
142
|
-
isRoot: true,
|
|
143
|
-
};
|
|
176
|
+
return { callerId: "root", callerType: "system", scopes: ["*"], isRoot: true };
|
|
144
177
|
}
|
|
145
178
|
|
|
146
|
-
// Validate token
|
|
147
179
|
const token = await authConfig.store.validateToken(credential);
|
|
148
180
|
if (!token) return null;
|
|
149
181
|
|
|
150
|
-
// Look up client name
|
|
151
182
|
const client = await authConfig.store.getClient(token.clientId);
|
|
152
|
-
|
|
153
183
|
return {
|
|
154
184
|
callerId: client?.name ?? token.clientId,
|
|
155
185
|
callerType: "agent",
|
|
@@ -158,42 +188,71 @@ async function resolveAuth(
|
|
|
158
188
|
};
|
|
159
189
|
}
|
|
160
190
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// ============================================
|
|
164
|
-
|
|
165
|
-
function canSeeAgent(
|
|
166
|
-
agent: AgentDefinition,
|
|
167
|
-
auth: ResolvedAuth | null,
|
|
168
|
-
): boolean {
|
|
169
|
-
const visibility: Visibility = agent.visibility ?? "internal";
|
|
170
|
-
|
|
191
|
+
function canSeeAgent(agent: AgentDefinition, auth: ResolvedAuth | null): boolean {
|
|
192
|
+
const visibility = (agent.config?.visibility ?? "internal") as Visibility;
|
|
171
193
|
if (auth?.isRoot) return true;
|
|
172
194
|
if (visibility === "public") return true;
|
|
173
195
|
if (visibility === "internal" && auth) return true;
|
|
174
196
|
return false;
|
|
175
197
|
}
|
|
176
198
|
|
|
199
|
+
// ============================================
|
|
200
|
+
// MCP Tool Definitions
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
function getToolDefinitions() {
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
name: "call_agent",
|
|
207
|
+
description:
|
|
208
|
+
"Execute a tool on a registered agent. Provide the agent path and tool name.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
request: {
|
|
213
|
+
type: "object",
|
|
214
|
+
description: "The call request",
|
|
215
|
+
properties: {
|
|
216
|
+
action: {
|
|
217
|
+
type: "string",
|
|
218
|
+
enum: ["execute_tool", "describe_tools", "load"],
|
|
219
|
+
description: "Action to perform",
|
|
220
|
+
},
|
|
221
|
+
path: {
|
|
222
|
+
type: "string",
|
|
223
|
+
description: "Agent path (e.g. '@registry')",
|
|
224
|
+
},
|
|
225
|
+
tool: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Tool name to call (for execute_tool)",
|
|
228
|
+
},
|
|
229
|
+
params: {
|
|
230
|
+
type: "object",
|
|
231
|
+
description: "Parameters for the tool",
|
|
232
|
+
additionalProperties: true,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
required: ["action", "path"],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
required: ["request"],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "list_agents",
|
|
243
|
+
description: "List all registered agents and their available tools.",
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: "object",
|
|
246
|
+
properties: {},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
177
252
|
// ============================================
|
|
178
253
|
// Create Server
|
|
179
254
|
// ============================================
|
|
180
255
|
|
|
181
|
-
/**
|
|
182
|
-
* Create an HTTP server for the agent registry.
|
|
183
|
-
*
|
|
184
|
-
* @example
|
|
185
|
-
* ```typescript
|
|
186
|
-
* const registry = createAgentRegistry();
|
|
187
|
-
* registry.register(createAuthAgent({ rootKey: 'rk_xxx' }));
|
|
188
|
-
* registry.register(myAgent);
|
|
189
|
-
*
|
|
190
|
-
* const server = createAgentServer(registry, { port: 3000 });
|
|
191
|
-
* await server.start();
|
|
192
|
-
* // POST /call - Execute agent actions
|
|
193
|
-
* // GET /list - List agents (filtered by auth)
|
|
194
|
-
* // POST /oauth/token - OAuth2 token endpoint
|
|
195
|
-
* ```
|
|
196
|
-
*/
|
|
197
256
|
export function createAgentServer(
|
|
198
257
|
registry: AgentRegistry,
|
|
199
258
|
options: AgentServerOptions = {},
|
|
@@ -203,31 +262,210 @@ export function createAgentServer(
|
|
|
203
262
|
hostname = "localhost",
|
|
204
263
|
basePath = "",
|
|
205
264
|
cors = true,
|
|
206
|
-
|
|
265
|
+
serverName = "agents-sdk",
|
|
266
|
+
serverVersion = "1.0.0",
|
|
207
267
|
} = options;
|
|
208
268
|
|
|
209
269
|
let serverInstance: ReturnType<typeof Bun.serve> | null = null;
|
|
210
270
|
let serverUrl: string | null = null;
|
|
211
271
|
|
|
212
|
-
// Detect auth configuration
|
|
213
272
|
const authConfig = detectAuth(registry);
|
|
214
273
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
274
|
+
// ──────────────────────────────────────────
|
|
275
|
+
// MCP JSON-RPC handler
|
|
276
|
+
// ──────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
async function handleJsonRpc(
|
|
279
|
+
request: JsonRpcRequest,
|
|
280
|
+
auth: ResolvedAuth | null,
|
|
281
|
+
): Promise<JsonRpcResponse> {
|
|
282
|
+
switch (request.method) {
|
|
283
|
+
// MCP protocol handshake
|
|
284
|
+
case "initialize":
|
|
285
|
+
return jsonRpcSuccess(request.id, {
|
|
286
|
+
protocolVersion: "2024-11-05",
|
|
287
|
+
capabilities: { tools: {} },
|
|
288
|
+
serverInfo: { name: serverName, version: serverVersion },
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
case "notifications/initialized":
|
|
292
|
+
return jsonRpcSuccess(request.id, {});
|
|
293
|
+
|
|
294
|
+
// List MCP tools
|
|
295
|
+
case "tools/list":
|
|
296
|
+
return jsonRpcSuccess(request.id, {
|
|
297
|
+
tools: getToolDefinitions(),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Call an MCP tool
|
|
301
|
+
case "tools/call": {
|
|
302
|
+
const { name, arguments: args } = (request.params ?? {}) as {
|
|
303
|
+
name: string;
|
|
304
|
+
arguments?: Record<string, unknown>;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const result = await handleToolCall(name, args ?? {}, auth);
|
|
309
|
+
return jsonRpcSuccess(request.id, result);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
return jsonRpcSuccess(
|
|
312
|
+
request.id,
|
|
313
|
+
mcpResult(
|
|
314
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
315
|
+
true,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
default:
|
|
322
|
+
return jsonRpcError(
|
|
323
|
+
request.id,
|
|
324
|
+
-32601,
|
|
325
|
+
`Method not found: ${request.method}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ──────────────────────────────────────────
|
|
331
|
+
// MCP tool implementations
|
|
332
|
+
// ──────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
async function handleToolCall(
|
|
335
|
+
toolName: string,
|
|
336
|
+
args: Record<string, unknown>,
|
|
337
|
+
auth: ResolvedAuth | null,
|
|
338
|
+
) {
|
|
339
|
+
switch (toolName) {
|
|
340
|
+
case "call_agent": {
|
|
341
|
+
const req = (args.request ?? args) as CallAgentRequest;
|
|
342
|
+
|
|
343
|
+
// Inject auth context
|
|
344
|
+
if (auth) {
|
|
345
|
+
req.callerId = auth.callerId;
|
|
346
|
+
req.callerType = auth.callerType;
|
|
347
|
+
if (!req.metadata) req.metadata = {};
|
|
348
|
+
req.metadata.scopes = auth.scopes;
|
|
349
|
+
req.metadata.isRoot = auth.isRoot;
|
|
350
|
+
}
|
|
351
|
+
if (auth?.isRoot) {
|
|
352
|
+
req.callerType = "system";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const result = await registry.call(req);
|
|
356
|
+
return mcpResult(result);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case "list_agents": {
|
|
360
|
+
const agents = registry.list();
|
|
361
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
362
|
+
|
|
363
|
+
return mcpResult({
|
|
364
|
+
success: true,
|
|
365
|
+
agents: visible.map((agent) => ({
|
|
366
|
+
path: agent.path,
|
|
367
|
+
name: agent.config?.name,
|
|
368
|
+
description: agent.config?.description,
|
|
369
|
+
supportedActions: agent.config?.supportedActions,
|
|
370
|
+
tools: agent.tools
|
|
371
|
+
.filter((t) => {
|
|
372
|
+
const tv = t.visibility ?? "internal";
|
|
373
|
+
if (auth?.isRoot) return true;
|
|
374
|
+
if (tv === "public") return true;
|
|
375
|
+
if (tv === "internal" && auth) return true;
|
|
376
|
+
return false;
|
|
377
|
+
})
|
|
378
|
+
.map((t) => t.name),
|
|
379
|
+
})),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
default:
|
|
384
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ──────────────────────────────────────────
|
|
389
|
+
// OAuth2 token handler (unchanged)
|
|
390
|
+
// ──────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
async function handleOAuthToken(req: Request): Promise<Response> {
|
|
393
|
+
if (!authConfig) {
|
|
394
|
+
return jsonResponse({ error: "auth_not_configured" }, 404);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const contentType = req.headers.get("Content-Type") ?? "";
|
|
398
|
+
let grantType: string;
|
|
399
|
+
let clientId: string;
|
|
400
|
+
let clientSecret: string;
|
|
401
|
+
|
|
402
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
403
|
+
const body = await req.text();
|
|
404
|
+
const params = new URLSearchParams(body);
|
|
405
|
+
grantType = params.get("grant_type") ?? "";
|
|
406
|
+
clientId = params.get("client_id") ?? "";
|
|
407
|
+
clientSecret = params.get("client_secret") ?? "";
|
|
408
|
+
} else {
|
|
409
|
+
const body = (await req.json()) as Record<string, string>;
|
|
410
|
+
grantType = body.grant_type ?? "";
|
|
411
|
+
clientId = body.client_id ?? "";
|
|
412
|
+
clientSecret = body.client_secret ?? "";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (grantType !== "client_credentials") {
|
|
416
|
+
return jsonResponse(
|
|
417
|
+
{ error: "unsupported_grant_type", error_description: "Only client_credentials is supported" },
|
|
418
|
+
400,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!clientId || !clientSecret) {
|
|
423
|
+
return jsonResponse(
|
|
424
|
+
{ error: "invalid_request", error_description: "Missing client_id or client_secret" },
|
|
425
|
+
400,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const client = await authConfig.store.validateClient(clientId, clientSecret);
|
|
430
|
+
if (!client) {
|
|
431
|
+
return jsonResponse(
|
|
432
|
+
{ error: "invalid_client", error_description: "Invalid client credentials" },
|
|
433
|
+
401,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const tokenString = `at_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
|
|
440
|
+
await authConfig.store.storeToken({
|
|
441
|
+
token: tokenString,
|
|
442
|
+
clientId: client.clientId,
|
|
443
|
+
scopes: client.scopes,
|
|
444
|
+
issuedAt: now,
|
|
445
|
+
expiresAt: now + authConfig.tokenTtl * 1000,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return jsonResponse({
|
|
449
|
+
access_token: tokenString,
|
|
450
|
+
token_type: "Bearer",
|
|
451
|
+
expires_in: authConfig.tokenTtl,
|
|
452
|
+
scope: client.scopes.join(" "),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ──────────────────────────────────────────
|
|
457
|
+
// HTTP request handler
|
|
458
|
+
// ─��────────────────────────────────────────
|
|
459
|
+
|
|
218
460
|
async function fetch(req: Request): Promise<Response> {
|
|
219
461
|
const url = new URL(req.url);
|
|
220
462
|
const path = url.pathname.replace(basePath, "") || "/";
|
|
221
463
|
|
|
222
|
-
//
|
|
464
|
+
// CORS preflight
|
|
223
465
|
if (cors && req.method === "OPTIONS") {
|
|
224
|
-
return new Response(null, {
|
|
225
|
-
status: 204,
|
|
226
|
-
headers: corsHeaders(),
|
|
227
|
-
});
|
|
466
|
+
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
228
467
|
}
|
|
229
468
|
|
|
230
|
-
// Add CORS headers to response
|
|
231
469
|
const addCors = (response: Response): Response => {
|
|
232
470
|
if (!cors) return response;
|
|
233
471
|
const headers = new Headers(response.headers);
|
|
@@ -241,133 +479,30 @@ export function createAgentServer(
|
|
|
241
479
|
});
|
|
242
480
|
};
|
|
243
481
|
|
|
244
|
-
// Resolve auth on every request
|
|
245
482
|
const auth = authConfig ? await resolveAuth(req, authConfig) : null;
|
|
246
483
|
|
|
247
484
|
try {
|
|
248
|
-
// POST /
|
|
249
|
-
if (path === "/
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
let clientSecret: string;
|
|
254
|
-
|
|
255
|
-
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
256
|
-
const body = await req.text();
|
|
257
|
-
const params = new URLSearchParams(body);
|
|
258
|
-
grantType = params.get("grant_type") ?? "";
|
|
259
|
-
clientId = params.get("client_id") ?? "";
|
|
260
|
-
clientSecret = params.get("client_secret") ?? "";
|
|
261
|
-
} else {
|
|
262
|
-
const body = (await req.json()) as Record<string, string>;
|
|
263
|
-
grantType = body.grant_type ?? "";
|
|
264
|
-
clientId = body.client_id ?? "";
|
|
265
|
-
clientSecret = body.client_secret ?? "";
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (grantType !== "client_credentials") {
|
|
269
|
-
return addCors(
|
|
270
|
-
jsonResponse(
|
|
271
|
-
{
|
|
272
|
-
error: "unsupported_grant_type",
|
|
273
|
-
error_description: "Only client_credentials is supported",
|
|
274
|
-
},
|
|
275
|
-
400,
|
|
276
|
-
),
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (!clientId || !clientSecret) {
|
|
281
|
-
return addCors(
|
|
282
|
-
jsonResponse(
|
|
283
|
-
{
|
|
284
|
-
error: "invalid_request",
|
|
285
|
-
error_description: "Missing client_id or client_secret",
|
|
286
|
-
},
|
|
287
|
-
400,
|
|
288
|
-
),
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const client = await authConfig.store.validateClient(
|
|
293
|
-
clientId,
|
|
294
|
-
clientSecret,
|
|
295
|
-
);
|
|
296
|
-
if (!client) {
|
|
297
|
-
return addCors(
|
|
298
|
-
jsonResponse(
|
|
299
|
-
{
|
|
300
|
-
error: "invalid_client",
|
|
301
|
-
error_description: "Invalid client credentials",
|
|
302
|
-
},
|
|
303
|
-
401,
|
|
304
|
-
),
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Generate token
|
|
309
|
-
const tokenString = `at_${Array.from({ length: 48 }, () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 62)]).join("")}`;
|
|
310
|
-
const token = {
|
|
311
|
-
token: tokenString,
|
|
312
|
-
clientId: client.clientId,
|
|
313
|
-
scopes: client.scopes,
|
|
314
|
-
issuedAt: Date.now(),
|
|
315
|
-
expiresAt: Date.now() + authConfig.tokenTtl * 1000,
|
|
316
|
-
};
|
|
317
|
-
await authConfig.store.storeToken(token);
|
|
318
|
-
|
|
319
|
-
// Standard OAuth2 response
|
|
320
|
-
return addCors(
|
|
321
|
-
jsonResponse({
|
|
322
|
-
access_token: token.token,
|
|
323
|
-
token_type: "bearer",
|
|
324
|
-
expires_in: authConfig.tokenTtl,
|
|
325
|
-
scope: client.scopes.join(" "),
|
|
326
|
-
}),
|
|
327
|
-
);
|
|
485
|
+
// MCP endpoint: POST / or POST /mcp
|
|
486
|
+
if ((path === "/" || path === "/mcp") && req.method === "POST") {
|
|
487
|
+
const body = (await req.json()) as JsonRpcRequest;
|
|
488
|
+
const response = await handleJsonRpc(body, auth);
|
|
489
|
+
return addCors(jsonResponse(response));
|
|
328
490
|
}
|
|
329
491
|
|
|
330
|
-
//
|
|
331
|
-
if (path === "/
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!body.path || !body.action) {
|
|
335
|
-
return addCors(
|
|
336
|
-
jsonResponse(
|
|
337
|
-
{
|
|
338
|
-
success: false,
|
|
339
|
-
error: "Missing required fields: path, action",
|
|
340
|
-
code: "INVALID_REQUEST",
|
|
341
|
-
},
|
|
342
|
-
400,
|
|
343
|
-
),
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Inject auth context into request
|
|
348
|
-
if (auth) {
|
|
349
|
-
body.callerId = auth.callerId;
|
|
350
|
-
body.callerType = auth.callerType;
|
|
351
|
-
if (!body.metadata) body.metadata = {};
|
|
352
|
-
body.metadata.scopes = auth.scopes;
|
|
353
|
-
body.metadata.isRoot = auth.isRoot;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Root key bypasses all access checks
|
|
357
|
-
if (auth?.isRoot) {
|
|
358
|
-
body.callerType = "system";
|
|
359
|
-
}
|
|
492
|
+
// OAuth2 token endpoint
|
|
493
|
+
if (path === "/oauth/token" && req.method === "POST") {
|
|
494
|
+
return addCors(await handleOAuthToken(req));
|
|
495
|
+
}
|
|
360
496
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return addCors(jsonResponse(
|
|
497
|
+
// Health check
|
|
498
|
+
if (path === "/health" && req.method === "GET") {
|
|
499
|
+
return addCors(jsonResponse({ status: "ok" }));
|
|
364
500
|
}
|
|
365
501
|
|
|
366
|
-
// GET /list
|
|
502
|
+
// Backwards compat: GET /list (returns agents directly)
|
|
367
503
|
if (path === "/list" && req.method === "GET") {
|
|
368
504
|
const agents = registry.list();
|
|
369
505
|
const visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
370
|
-
|
|
371
506
|
return addCors(
|
|
372
507
|
jsonResponse({
|
|
373
508
|
success: true,
|
|
@@ -390,55 +525,37 @@ export function createAgentServer(
|
|
|
390
525
|
);
|
|
391
526
|
}
|
|
392
527
|
|
|
393
|
-
|
|
394
|
-
if (onNotFound) {
|
|
395
|
-
return addCors(await onNotFound(req));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return addCors(
|
|
399
|
-
jsonResponse(
|
|
400
|
-
{
|
|
401
|
-
success: false,
|
|
402
|
-
error: `Not found: ${req.method} ${path}`,
|
|
403
|
-
code: "NOT_FOUND",
|
|
404
|
-
},
|
|
405
|
-
404,
|
|
406
|
-
),
|
|
407
|
-
);
|
|
528
|
+
return addCors(jsonResponse({ jsonrpc: "2.0", id: null, error: { code: -32601, message: `Not found: ${req.method} ${path}` } }, 404));
|
|
408
529
|
} catch (err) {
|
|
409
530
|
return addCors(
|
|
410
531
|
jsonResponse(
|
|
411
|
-
{
|
|
412
|
-
success: false,
|
|
413
|
-
error: err instanceof Error ? err.message : String(err),
|
|
414
|
-
code: "INTERNAL_ERROR",
|
|
415
|
-
},
|
|
532
|
+
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "Internal error" } },
|
|
416
533
|
500,
|
|
417
534
|
),
|
|
418
535
|
);
|
|
419
536
|
}
|
|
420
537
|
}
|
|
421
538
|
|
|
539
|
+
// ──────────────────────────────────────────
|
|
540
|
+
// Server lifecycle
|
|
541
|
+
// ──────────────────────────────────────────
|
|
542
|
+
|
|
422
543
|
const server: AgentServer = {
|
|
423
544
|
async start(): Promise<void> {
|
|
424
|
-
if (serverInstance)
|
|
425
|
-
throw new Error("Server is already running");
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
serverInstance = Bun.serve({
|
|
429
|
-
port,
|
|
430
|
-
hostname,
|
|
431
|
-
fetch,
|
|
432
|
-
});
|
|
545
|
+
if (serverInstance) throw new Error("Server is already running");
|
|
433
546
|
|
|
547
|
+
serverInstance = Bun.serve({ port, hostname, fetch });
|
|
434
548
|
serverUrl = `http://${hostname}:${port}${basePath}`;
|
|
549
|
+
|
|
435
550
|
console.log(`Agent server running at ${serverUrl}`);
|
|
436
|
-
console.log(` POST
|
|
437
|
-
console.log(`
|
|
551
|
+
console.log(` POST / - MCP JSON-RPC endpoint`);
|
|
552
|
+
console.log(` POST /mcp - MCP JSON-RPC endpoint (alias)`);
|
|
553
|
+
console.log(` GET /health - Health check`);
|
|
438
554
|
if (authConfig) {
|
|
439
|
-
console.log(` POST
|
|
440
|
-
console.log(" Auth: enabled
|
|
555
|
+
console.log(` POST /oauth/token - OAuth2 token endpoint`);
|
|
556
|
+
console.log(" Auth: enabled");
|
|
441
557
|
}
|
|
558
|
+
console.log(` MCP tools: call_agent, list_agents`);
|
|
442
559
|
},
|
|
443
560
|
|
|
444
561
|
async stop(): Promise<void> {
|