apteva 0.4.15 → 0.4.16

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.
@@ -0,0 +1,387 @@
1
+ // Local MCP server handler
2
+ // Handles JSON-RPC requests for servers of type "local"
3
+ // Tools are stored in the database with configurable handler types: mock, http, javascript
4
+
5
+ import { McpServerDB, McpServerToolDB, type McpServerTool } from "./db";
6
+
7
+ const PROTOCOL_VERSION = "2024-11-05";
8
+
9
+ interface JsonRpcRequest {
10
+ jsonrpc: "2.0";
11
+ id: number;
12
+ method: string;
13
+ params?: any;
14
+ }
15
+
16
+ interface JsonRpcResponse {
17
+ jsonrpc: "2.0";
18
+ id: number;
19
+ result?: unknown;
20
+ error?: { code: number; message: string; data?: unknown };
21
+ }
22
+
23
+ // Template helpers available in mock_response and javascript handlers
24
+ function templateHelpers() {
25
+ return {
26
+ uuid: () => crypto.randomUUID(),
27
+ now: new Date().toISOString(),
28
+ timestamp: Date.now(),
29
+ random_int: (min: number, max: number) =>
30
+ Math.floor(Math.random() * (max - min + 1)) + min,
31
+ random_float: (min: number, max: number) =>
32
+ Math.random() * (max - min) + min,
33
+ };
34
+ }
35
+
36
+ // Render mock response template with variable substitution
37
+ // Supports: {{args.field}}, {{uuid()}}, {{now}}, {{timestamp}}, {{random_int(min,max)}}
38
+ function renderTemplate(template: any, args: Record<string, any>): any {
39
+ const helpers = templateHelpers();
40
+
41
+ if (typeof template === "string") {
42
+ // Check if entire string is a single expression
43
+ const fullMatch = template.match(/^\{\{(.+)\}\}$/);
44
+ if (fullMatch) {
45
+ return evaluateExpression(fullMatch[1].trim(), args, helpers);
46
+ }
47
+ // Replace embedded expressions
48
+ return template.replace(/\{\{([^}]+)\}\}/g, (_, expr) => {
49
+ const result = evaluateExpression(expr.trim(), args, helpers);
50
+ if (result === null || result === undefined) return "";
51
+ if (typeof result === "object") return JSON.stringify(result);
52
+ return String(result);
53
+ });
54
+ }
55
+
56
+ if (Array.isArray(template)) {
57
+ return template.map((item) => renderTemplate(item, args));
58
+ }
59
+
60
+ if (template !== null && typeof template === "object") {
61
+ const result: Record<string, any> = {};
62
+ for (const [key, val] of Object.entries(template)) {
63
+ result[key] = renderTemplate(val, args);
64
+ }
65
+ return result;
66
+ }
67
+
68
+ return template;
69
+ }
70
+
71
+ function evaluateExpression(
72
+ expr: string,
73
+ args: Record<string, any>,
74
+ helpers: ReturnType<typeof templateHelpers>,
75
+ ): any {
76
+ // Handle args.* references
77
+ if (expr.startsWith("args.")) {
78
+ const key = expr.slice(5);
79
+ return args[key] ?? null;
80
+ }
81
+
82
+ // Handle helper functions and values
83
+ try {
84
+ const fn = new Function(
85
+ "args",
86
+ "uuid",
87
+ "now",
88
+ "timestamp",
89
+ "random_int",
90
+ "random_float",
91
+ `return ${expr}`,
92
+ );
93
+ return fn(
94
+ args,
95
+ helpers.uuid,
96
+ helpers.now,
97
+ helpers.timestamp,
98
+ helpers.random_int,
99
+ helpers.random_float,
100
+ );
101
+ } catch {
102
+ return expr;
103
+ }
104
+ }
105
+
106
+ // Execute a mock handler — returns the rendered mock_response
107
+ function executeMock(
108
+ tool: McpServerTool,
109
+ args: Record<string, any>,
110
+ ): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
111
+ const mockResponse = tool.mock_response || {};
112
+ const rendered = renderTemplate(mockResponse, args);
113
+ return {
114
+ content: [{ type: "text", text: JSON.stringify(rendered, null, 2) }],
115
+ };
116
+ }
117
+
118
+ // Execute an HTTP handler — makes a real API call
119
+ async function executeHttp(
120
+ tool: McpServerTool,
121
+ args: Record<string, any>,
122
+ credentials: Record<string, string>,
123
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
124
+ const config = tool.http_config;
125
+ if (!config || !config.url) {
126
+ return {
127
+ content: [{ type: "text", text: "Error: No HTTP config or URL defined" }],
128
+ isError: true,
129
+ };
130
+ }
131
+
132
+ const { method = "GET", url, headers = {}, body } = config;
133
+
134
+ // Render templates in URL, headers, and body
135
+ const renderedUrl = renderTemplate(url, args) as string;
136
+ const renderedHeaders = renderTemplate(headers, args) as Record<string, string>;
137
+
138
+ // Substitute credential references in headers
139
+ const finalHeaders: Record<string, string> = { "Content-Type": "application/json" };
140
+ for (const [k, v] of Object.entries(renderedHeaders)) {
141
+ let val = String(v);
142
+ // Replace {{credential.*}} references
143
+ val = val.replace(/\{\{credential\.([^}]+)\}\}/g, (_, key) => credentials[key] || "");
144
+ finalHeaders[k] = val;
145
+ }
146
+
147
+ const fetchOptions: RequestInit = {
148
+ method: method.toUpperCase(),
149
+ headers: finalHeaders,
150
+ };
151
+
152
+ if (["POST", "PUT", "PATCH"].includes(fetchOptions.method!)) {
153
+ if (body) {
154
+ const renderedBody = renderTemplate(body, args);
155
+ fetchOptions.body = JSON.stringify(renderedBody);
156
+ } else {
157
+ fetchOptions.body = JSON.stringify(args);
158
+ }
159
+ }
160
+
161
+ try {
162
+ const response = await fetch(renderedUrl, fetchOptions);
163
+ const text = await response.text();
164
+
165
+ let data;
166
+ try {
167
+ data = JSON.parse(text);
168
+ } catch {
169
+ data = text;
170
+ }
171
+
172
+ if (!response.ok) {
173
+ return {
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: `HTTP ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}`,
178
+ },
179
+ ],
180
+ isError: true,
181
+ };
182
+ }
183
+
184
+ return {
185
+ content: [
186
+ {
187
+ type: "text",
188
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
189
+ },
190
+ ],
191
+ };
192
+ } catch (err) {
193
+ return {
194
+ content: [{ type: "text", text: `HTTP error: ${err}` }],
195
+ isError: true,
196
+ };
197
+ }
198
+ }
199
+
200
+ // Execute a JavaScript handler — runs code string with args + credentials
201
+ function executeJavascript(
202
+ tool: McpServerTool,
203
+ args: Record<string, any>,
204
+ credentials: Record<string, string>,
205
+ ): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
206
+ if (!tool.code) {
207
+ return {
208
+ content: [{ type: "text", text: "Error: No code defined for this tool" }],
209
+ isError: true,
210
+ };
211
+ }
212
+
213
+ try {
214
+ const helpers = templateHelpers();
215
+ const fn = new Function(
216
+ "args",
217
+ "credentials",
218
+ "uuid",
219
+ "now",
220
+ "timestamp",
221
+ "random_int",
222
+ "random_float",
223
+ tool.code,
224
+ );
225
+ const result = fn(
226
+ args,
227
+ credentials,
228
+ helpers.uuid,
229
+ helpers.now,
230
+ helpers.timestamp,
231
+ helpers.random_int,
232
+ helpers.random_float,
233
+ );
234
+ const text =
235
+ typeof result === "string" ? result : JSON.stringify(result, null, 2);
236
+ return { content: [{ type: "text", text }] };
237
+ } catch (err) {
238
+ return {
239
+ content: [{ type: "text", text: `JavaScript error: ${err}` }],
240
+ isError: true,
241
+ };
242
+ }
243
+ }
244
+
245
+ // Execute a tool based on its handler_type
246
+ async function executeTool(
247
+ tool: McpServerTool,
248
+ args: Record<string, any>,
249
+ credentials: Record<string, string>,
250
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
251
+ switch (tool.handler_type) {
252
+ case "http":
253
+ return executeHttp(tool, args, credentials);
254
+ case "javascript":
255
+ return executeJavascript(tool, args, credentials);
256
+ case "mock":
257
+ default:
258
+ return executeMock(tool, args);
259
+ }
260
+ }
261
+
262
+ // Main JSON-RPC handler for local MCP servers
263
+ export async function handleLocalMcpRequest(
264
+ req: Request,
265
+ serverId: string,
266
+ ): Promise<Response> {
267
+ const corsHeaders = {
268
+ "Access-Control-Allow-Origin": "*",
269
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
270
+ "Access-Control-Allow-Headers": "Content-Type, Mcp-Session-Id",
271
+ };
272
+
273
+ if (req.method === "OPTIONS") {
274
+ return new Response(null, { headers: corsHeaders });
275
+ }
276
+
277
+ const server = McpServerDB.findById(serverId);
278
+ if (!server || server.type !== "local") {
279
+ return new Response(
280
+ JSON.stringify({
281
+ jsonrpc: "2.0",
282
+ id: 0,
283
+ error: { code: -32600, message: "Server not found or not a local server" },
284
+ }),
285
+ { headers: { ...corsHeaders, "Content-Type": "application/json" } },
286
+ );
287
+ }
288
+
289
+ let body: JsonRpcRequest;
290
+ try {
291
+ body = (await req.json()) as JsonRpcRequest;
292
+ } catch {
293
+ return new Response(
294
+ JSON.stringify({
295
+ jsonrpc: "2.0",
296
+ id: 0,
297
+ error: { code: -32700, message: "Parse error" },
298
+ }),
299
+ { headers: { ...corsHeaders, "Content-Type": "application/json" } },
300
+ );
301
+ }
302
+
303
+ const { method, params, id } = body;
304
+ let result: unknown;
305
+ let error: { code: number; message: string } | undefined;
306
+
307
+ // Parse server credentials
308
+ let credentials: Record<string, string> = {};
309
+ try {
310
+ if (server.env && Object.keys(server.env).length > 0) {
311
+ credentials = server.env;
312
+ }
313
+ } catch {
314
+ // ignore
315
+ }
316
+
317
+ switch (method) {
318
+ case "initialize": {
319
+ result = {
320
+ protocolVersion: PROTOCOL_VERSION,
321
+ capabilities: {
322
+ tools: { listChanged: false },
323
+ },
324
+ serverInfo: {
325
+ name: server.name,
326
+ version: "1.0.0",
327
+ },
328
+ };
329
+ break;
330
+ }
331
+
332
+ case "notifications/initialized": {
333
+ result = {};
334
+ break;
335
+ }
336
+
337
+ case "tools/list": {
338
+ const tools = McpServerToolDB.findByServer(serverId);
339
+ result = {
340
+ tools: tools
341
+ .filter((t) => t.enabled)
342
+ .map((t) => ({
343
+ name: t.name,
344
+ description: t.description,
345
+ inputSchema: t.input_schema,
346
+ })),
347
+ };
348
+ break;
349
+ }
350
+
351
+ case "tools/call": {
352
+ const { name, arguments: args } = params as {
353
+ name: string;
354
+ arguments: Record<string, any>;
355
+ };
356
+ const tool = McpServerToolDB.findByServerAndName(serverId, name);
357
+ if (!tool) {
358
+ result = {
359
+ content: [{ type: "text", text: `Tool '${name}' not found` }],
360
+ isError: true,
361
+ };
362
+ } else if (!tool.enabled) {
363
+ result = {
364
+ content: [{ type: "text", text: `Tool '${name}' is disabled` }],
365
+ isError: true,
366
+ };
367
+ } else {
368
+ result = await executeTool(tool, args || {}, credentials);
369
+ }
370
+ break;
371
+ }
372
+
373
+ default: {
374
+ error = { code: -32601, message: `Method not found: ${method}` };
375
+ }
376
+ }
377
+
378
+ const response: JsonRpcResponse = {
379
+ jsonrpc: "2.0",
380
+ id: id || 0,
381
+ ...(error ? { error } : { result }),
382
+ };
383
+
384
+ return new Response(JSON.stringify(response), {
385
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
386
+ });
387
+ }
@@ -126,7 +126,17 @@ export function buildAgentConfig(agent: Agent, providerKey: string) {
126
126
  const server = McpServerDB.findById(id);
127
127
  if (!server) continue;
128
128
 
129
- if (server.type === "http" && server.url) {
129
+ if (server.type === "local" && server.status === "running") {
130
+ // Local MCP server (in-process, no subprocess)
131
+ const baseUrl = `http://localhost:${process.env.PORT || 4280}`;
132
+ mcpServers.push({
133
+ name: server.name,
134
+ type: "http",
135
+ url: `${baseUrl}/api/mcp/servers/${server.id}/mcp`,
136
+ headers: {},
137
+ enabled: true,
138
+ });
139
+ } else if (server.type === "http" && server.url) {
130
140
  // Remote HTTP server (Composio, Smithery, or custom)
131
141
  mcpServers.push({
132
142
  name: server.name,
@@ -136,7 +146,7 @@ export function buildAgentConfig(agent: Agent, providerKey: string) {
136
146
  enabled: true,
137
147
  });
138
148
  } else if (server.status === "running" && server.port) {
139
- // Local MCP server (npm, github, custom)
149
+ // Subprocess MCP server (npm, github, custom)
140
150
  mcpServers.push({
141
151
  name: server.name,
142
152
  type: "http",
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "bun";
2
2
  import { json } from "./helpers";
3
- import { McpServerDB, generateId, type McpServer } from "../../db";
3
+ import { McpServerDB, McpServerToolDB, generateId, type McpServer } from "../../db";
4
4
  import { getNextPort } from "../../server";
5
5
  import {
6
6
  startMcpProcess,
@@ -11,6 +11,7 @@ import {
11
11
  getMcpProcess,
12
12
  getHttpMcpClient,
13
13
  } from "../../mcp-client";
14
+ import { handleLocalMcpRequest } from "../../mcp-handler";
14
15
 
15
16
  export async function handleMcpRoutes(
16
17
  req: Request,
@@ -121,6 +122,12 @@ export async function handleMcpRoutes(
121
122
  }
122
123
  }
123
124
 
125
+ // POST /api/mcp/servers/:id/mcp - JSON-RPC endpoint for local MCP servers
126
+ const mcpJsonRpcMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/mcp$/);
127
+ if (mcpJsonRpcMatch && method === "POST") {
128
+ return handleLocalMcpRequest(req, mcpJsonRpcMatch[1]);
129
+ }
130
+
124
131
  // GET /api/mcp/servers/:id - Get a specific MCP server
125
132
  const mcpServerMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)$/);
126
133
  if (mcpServerMatch && method === "GET") {
@@ -168,8 +175,13 @@ export async function handleMcpRoutes(
168
175
  }
169
176
 
170
177
  // Stop if running
171
- if (server.status === "running") {
172
- // TODO: Stop the server process
178
+ if (server.status === "running" && server.type !== "local") {
179
+ stopMcpProcess(server.id);
180
+ }
181
+
182
+ // Delete tools if local server
183
+ if (server.type === "local") {
184
+ McpServerToolDB.deleteByServer(mcpServerMatch[1]);
173
185
  }
174
186
 
175
187
  McpServerDB.delete(mcpServerMatch[1]);
@@ -188,6 +200,19 @@ export async function handleMcpRoutes(
188
200
  return json({ error: "MCP server already running" }, 400);
189
201
  }
190
202
 
203
+ // Local servers: just flip status and set the MCP endpoint URL
204
+ if (server.type === "local") {
205
+ const updated = McpServerDB.update(server.id, {
206
+ status: "running",
207
+ url: `/api/mcp/servers/${server.id}/mcp`,
208
+ });
209
+ return json({
210
+ server: updated,
211
+ message: "Local MCP server started",
212
+ mcpUrl: `/api/mcp/servers/${server.id}/mcp`,
213
+ });
214
+ }
215
+
191
216
  // Determine command to run
192
217
  // Helper to substitute $ENV_VAR references with actual values
193
218
  const substituteEnvVars = (str: string, env: Record<string, string>): string => {
@@ -276,6 +301,12 @@ export async function handleMcpRoutes(
276
301
  return json({ error: "MCP server not found" }, 404);
277
302
  }
278
303
 
304
+ // Local servers: just flip status
305
+ if (server.type === "local") {
306
+ const updated = McpServerDB.update(server.id, { status: "stopped" });
307
+ return json({ server: updated, message: "Local MCP server stopped" });
308
+ }
309
+
279
310
  // Stop the MCP process
280
311
  stopMcpProcess(server.id);
281
312
 
@@ -291,6 +322,22 @@ export async function handleMcpRoutes(
291
322
  return json({ error: "MCP server not found" }, 404);
292
323
  }
293
324
 
325
+ // Local servers: read tools from database
326
+ if (server.type === "local") {
327
+ const tools = McpServerToolDB.findByServer(server.id);
328
+ return json({
329
+ serverInfo: { name: server.name, version: "1.0.0" },
330
+ tools: tools.map((t) => ({
331
+ id: t.id,
332
+ name: t.name,
333
+ description: t.description,
334
+ inputSchema: t.input_schema,
335
+ handler_type: t.handler_type,
336
+ enabled: t.enabled,
337
+ })),
338
+ });
339
+ }
340
+
294
341
  // HTTP servers use remote HTTP transport
295
342
  if (server.type === "http" && server.url) {
296
343
  try {
@@ -328,6 +375,101 @@ export async function handleMcpRoutes(
328
375
  }
329
376
  }
330
377
 
378
+ // POST /api/mcp/servers/:id/tools - Add a tool to a local MCP server
379
+ if (mcpToolsMatch && method === "POST") {
380
+ const server = McpServerDB.findById(mcpToolsMatch[1]);
381
+ if (!server) {
382
+ return json({ error: "MCP server not found" }, 404);
383
+ }
384
+ if (server.type !== "local") {
385
+ return json({ error: "Tools can only be added to local servers" }, 400);
386
+ }
387
+
388
+ try {
389
+ const body = await req.json();
390
+ if (!body.name || !body.description) {
391
+ return json({ error: "name and description are required" }, 400);
392
+ }
393
+
394
+ // Check for duplicate tool name
395
+ const existing = McpServerToolDB.findByServerAndName(server.id, body.name);
396
+ if (existing) {
397
+ return json({ error: `Tool '${body.name}' already exists on this server` }, 409);
398
+ }
399
+
400
+ const tool = McpServerToolDB.create({
401
+ id: generateId(),
402
+ server_id: server.id,
403
+ name: body.name,
404
+ description: body.description,
405
+ input_schema: body.input_schema || { type: "object", properties: {} },
406
+ handler_type: body.handler_type || "mock",
407
+ mock_response: body.mock_response || null,
408
+ http_config: body.http_config || null,
409
+ code: body.code || null,
410
+ enabled: body.enabled !== false,
411
+ });
412
+
413
+ return json({ tool }, 201);
414
+ } catch (e) {
415
+ console.error("Create tool error:", e);
416
+ return json({ error: "Invalid request body" }, 400);
417
+ }
418
+ }
419
+
420
+ // PUT /api/mcp/servers/:id/tools/:toolId - Update a tool on a local MCP server
421
+ const mcpToolUpdateMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools\/([^/]+)$/);
422
+ if (mcpToolUpdateMatch && method === "PUT") {
423
+ const server = McpServerDB.findById(mcpToolUpdateMatch[1]);
424
+ if (!server) {
425
+ return json({ error: "MCP server not found" }, 404);
426
+ }
427
+ if (server.type !== "local") {
428
+ return json({ error: "Tools can only be updated on local servers" }, 400);
429
+ }
430
+
431
+ const tool = McpServerToolDB.findById(mcpToolUpdateMatch[2]);
432
+ if (!tool || tool.server_id !== server.id) {
433
+ return json({ error: "Tool not found" }, 404);
434
+ }
435
+
436
+ try {
437
+ const body = await req.json();
438
+ const updated = McpServerToolDB.update(tool.id, {
439
+ ...(body.name !== undefined && { name: body.name }),
440
+ ...(body.description !== undefined && { description: body.description }),
441
+ ...(body.input_schema !== undefined && { input_schema: body.input_schema }),
442
+ ...(body.handler_type !== undefined && { handler_type: body.handler_type }),
443
+ ...(body.mock_response !== undefined && { mock_response: body.mock_response }),
444
+ ...(body.http_config !== undefined && { http_config: body.http_config }),
445
+ ...(body.code !== undefined && { code: body.code }),
446
+ ...(body.enabled !== undefined && { enabled: body.enabled }),
447
+ });
448
+ return json({ tool: updated });
449
+ } catch (e) {
450
+ return json({ error: "Invalid request body" }, 400);
451
+ }
452
+ }
453
+
454
+ // DELETE /api/mcp/servers/:id/tools/:toolId - Delete a tool from a local MCP server
455
+ if (mcpToolUpdateMatch && method === "DELETE") {
456
+ const server = McpServerDB.findById(mcpToolUpdateMatch[1]);
457
+ if (!server) {
458
+ return json({ error: "MCP server not found" }, 404);
459
+ }
460
+ if (server.type !== "local") {
461
+ return json({ error: "Tools can only be deleted from local servers" }, 400);
462
+ }
463
+
464
+ const tool = McpServerToolDB.findById(mcpToolUpdateMatch[2]);
465
+ if (!tool || tool.server_id !== server.id) {
466
+ return json({ error: "Tool not found" }, 404);
467
+ }
468
+
469
+ McpServerToolDB.delete(tool.id);
470
+ return json({ success: true });
471
+ }
472
+
331
473
  // POST /api/mcp/servers/:id/tools/:toolName/call - Call a tool on an MCP server
332
474
  const mcpToolCallMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools\/([^/]+)\/call$/);
333
475
  if (mcpToolCallMatch && method === "POST") {
@@ -338,6 +480,35 @@ export async function handleMcpRoutes(
338
480
 
339
481
  const toolName = decodeURIComponent(mcpToolCallMatch[2]);
340
482
 
483
+ // Local servers: execute tool handler directly
484
+ if (server.type === "local") {
485
+ const tool = McpServerToolDB.findByServerAndName(server.id, toolName);
486
+ if (!tool) {
487
+ return json({ error: `Tool '${toolName}' not found` }, 404);
488
+ }
489
+ if (!tool.enabled) {
490
+ return json({ error: `Tool '${toolName}' is disabled` }, 400);
491
+ }
492
+
493
+ // Forward to JSON-RPC handler via a synthetic request
494
+ const syntheticReq = new Request(req.url, {
495
+ method: "POST",
496
+ headers: { "Content-Type": "application/json" },
497
+ body: JSON.stringify({
498
+ jsonrpc: "2.0",
499
+ id: 1,
500
+ method: "tools/call",
501
+ params: {
502
+ name: toolName,
503
+ arguments: (await req.json()).arguments || {},
504
+ },
505
+ }),
506
+ });
507
+ const mcpResponse = await handleLocalMcpRequest(syntheticReq, server.id);
508
+ const mcpResult = await mcpResponse.json() as any;
509
+ return json({ result: mcpResult.result });
510
+ }
511
+
341
512
  // HTTP servers use remote HTTP transport
342
513
  if (server.type === "http" && server.url) {
343
514
  try {
package/src/server.ts CHANGED
@@ -357,7 +357,7 @@ const server = Bun.serve({
357
357
  const corsHeaders = {
358
358
  "Access-Control-Allow-Origin": allowOrigin,
359
359
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
360
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
360
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
361
361
  "Access-Control-Allow-Credentials": "true",
362
362
  };
363
363
 
package/src/web/App.tsx CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  CreateAgentModal,
24
24
  AgentsView,
25
25
  Dashboard,
26
+ ActivityPage,
26
27
  TasksPage,
27
28
  McpPage,
28
29
  SkillsPage,
@@ -254,6 +255,13 @@ function AppContent() {
254
255
  <main className="flex-1 overflow-hidden flex">
255
256
  {route === "settings" && <SettingsPage />}
256
257
 
258
+ {route === "activity" && (
259
+ <ActivityPage
260
+ agents={agents}
261
+ loading={loading}
262
+ />
263
+ )}
264
+
257
265
  {route === "agents" && (
258
266
  <AgentsView
259
267
  agents={agents}