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.
- package/dist/App.2194efgj.js +228 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/middleware.ts +10 -0
- package/src/db.ts +159 -4
- package/src/mcp-handler.ts +387 -0
- package/src/routes/api/agent-utils.ts +12 -2
- package/src/routes/api/mcp.ts +174 -3
- package/src/server.ts +1 -1
- package/src/web/App.tsx +8 -0
- package/src/web/components/activity/ActivityPage.tsx +326 -0
- package/src/web/components/activity/index.ts +1 -0
- package/src/web/components/common/Icons.tsx +35 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/dashboard/Dashboard.tsx +81 -1
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/tasks/TasksPage.tsx +122 -15
- package/src/web/types.ts +1 -1
- package/dist/App.jdzxkzm1.js +0 -228
|
@@ -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 === "
|
|
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
|
-
//
|
|
149
|
+
// Subprocess MCP server (npm, github, custom)
|
|
140
150
|
mcpServers.push({
|
|
141
151
|
name: server.name,
|
|
142
152
|
type: "http",
|
package/src/routes/api/mcp.ts
CHANGED
|
@@ -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
|
-
|
|
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}
|