apteva 0.2.3 → 0.2.5
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.ggy88vnx.js +213 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
- package/src/binary.ts +271 -1
- package/src/crypto.ts +53 -0
- package/src/db.ts +492 -3
- package/src/mcp-client.ts +599 -0
- package/src/providers.ts +31 -0
- package/src/routes/api.ts +786 -63
- package/src/server.ts +122 -5
- package/src/web/App.tsx +36 -1
- package/src/web/components/agents/AgentCard.tsx +22 -1
- package/src/web/components/agents/AgentPanel.tsx +381 -0
- package/src/web/components/agents/AgentsView.tsx +27 -10
- package/src/web/components/agents/CreateAgentModal.tsx +7 -7
- package/src/web/components/agents/index.ts +1 -1
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Modal.tsx +2 -2
- package/src/web/components/common/Select.tsx +1 -1
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/dashboard/Dashboard.tsx +74 -25
- package/src/web/components/index.ts +5 -2
- package/src/web/components/layout/Sidebar.tsx +22 -2
- package/src/web/components/mcp/McpPage.tsx +1144 -0
- package/src/web/components/mcp/index.ts +1 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
- package/src/web/components/settings/SettingsPage.tsx +312 -82
- package/src/web/components/tasks/TasksPage.tsx +129 -0
- package/src/web/components/tasks/index.ts +1 -0
- package/src/web/components/telemetry/TelemetryPage.tsx +316 -0
- package/src/web/hooks/useAgents.ts +23 -0
- package/src/web/styles.css +18 -0
- package/src/web/types.ts +75 -1
- package/dist/App.wfhmfhx7.js +0 -213
- package/src/web/components/agents/ChatPanel.tsx +0 -63
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
// MCP Client for communicating with MCP servers
|
|
2
|
+
// Supports both stdio (subprocess) and HTTP transports
|
|
3
|
+
// Includes HTTP proxy to expose stdio servers over HTTP for agents
|
|
4
|
+
|
|
5
|
+
import { spawn, serve, type Subprocess, type Server } from "bun";
|
|
6
|
+
|
|
7
|
+
export interface McpTool {
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
inputSchema: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface McpToolCallResult {
|
|
14
|
+
content: McpContentBlock[];
|
|
15
|
+
isError?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface McpContentBlock {
|
|
19
|
+
type: "text" | "image" | "resource";
|
|
20
|
+
text?: string;
|
|
21
|
+
data?: string; // base64 for images
|
|
22
|
+
mimeType?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface JsonRpcRequest {
|
|
26
|
+
jsonrpc: "2.0";
|
|
27
|
+
id: number;
|
|
28
|
+
method: string;
|
|
29
|
+
params?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface JsonRpcResponse {
|
|
33
|
+
jsonrpc: "2.0";
|
|
34
|
+
id: number;
|
|
35
|
+
result?: unknown;
|
|
36
|
+
error?: {
|
|
37
|
+
code: number;
|
|
38
|
+
message: string;
|
|
39
|
+
data?: unknown;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface InitializeResult {
|
|
44
|
+
protocolVersion: string;
|
|
45
|
+
capabilities: {
|
|
46
|
+
tools?: { listChanged?: boolean };
|
|
47
|
+
resources?: { subscribe?: boolean; listChanged?: boolean };
|
|
48
|
+
prompts?: { listChanged?: boolean };
|
|
49
|
+
};
|
|
50
|
+
serverInfo: {
|
|
51
|
+
name: string;
|
|
52
|
+
version: string;
|
|
53
|
+
};
|
|
54
|
+
instructions?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ToolsListResult {
|
|
58
|
+
tools: McpTool[];
|
|
59
|
+
nextCursor?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
63
|
+
|
|
64
|
+
// ============ Stdio MCP Client ============
|
|
65
|
+
|
|
66
|
+
interface McpProcess {
|
|
67
|
+
proc: Subprocess;
|
|
68
|
+
initialized: boolean;
|
|
69
|
+
serverInfo: { name: string; version: string } | null;
|
|
70
|
+
requestId: number;
|
|
71
|
+
buffer: string;
|
|
72
|
+
httpServer?: Server; // HTTP proxy server
|
|
73
|
+
httpPort?: number; // Port the HTTP proxy is running on
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Store running MCP processes
|
|
77
|
+
const mcpProcesses = new Map<string, McpProcess>();
|
|
78
|
+
|
|
79
|
+
// Mutex for serializing stdio requests (one at a time per process)
|
|
80
|
+
const requestLocks = new Map<string, Promise<void>>();
|
|
81
|
+
|
|
82
|
+
async function withLock<T>(serverId: string, fn: () => Promise<T>): Promise<T> {
|
|
83
|
+
// Wait for any pending request to complete
|
|
84
|
+
while (requestLocks.has(serverId)) {
|
|
85
|
+
await requestLocks.get(serverId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let resolve: () => void;
|
|
89
|
+
const lockPromise = new Promise<void>(r => { resolve = r; });
|
|
90
|
+
requestLocks.set(serverId, lockPromise);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
return await fn();
|
|
94
|
+
} finally {
|
|
95
|
+
requestLocks.delete(serverId);
|
|
96
|
+
resolve!();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getMcpProcess(serverId: string): McpProcess | undefined {
|
|
101
|
+
return mcpProcesses.get(serverId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function startMcpProcess(
|
|
105
|
+
serverId: string,
|
|
106
|
+
command: string[],
|
|
107
|
+
env: Record<string, string> = {},
|
|
108
|
+
httpPort?: number
|
|
109
|
+
): Promise<{ success: boolean; error?: string; port?: number }> {
|
|
110
|
+
// Stop existing process if any
|
|
111
|
+
stopMcpProcess(serverId);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const proc = spawn({
|
|
115
|
+
cmd: command,
|
|
116
|
+
env: { ...process.env as Record<string, string>, ...env },
|
|
117
|
+
stdin: "pipe",
|
|
118
|
+
stdout: "pipe",
|
|
119
|
+
stderr: "pipe",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const entry: McpProcess = {
|
|
123
|
+
proc,
|
|
124
|
+
initialized: false,
|
|
125
|
+
serverInfo: null,
|
|
126
|
+
requestId: 0,
|
|
127
|
+
buffer: "",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
mcpProcesses.set(serverId, entry);
|
|
131
|
+
|
|
132
|
+
// Give it a moment to start
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
134
|
+
|
|
135
|
+
// Check if process is still running
|
|
136
|
+
if (proc.exitCode !== null) {
|
|
137
|
+
const stderr = await new Response(proc.stderr).text();
|
|
138
|
+
mcpProcesses.delete(serverId);
|
|
139
|
+
return { success: false, error: `Process exited: ${stderr || "unknown error"}` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Start HTTP proxy server if port specified
|
|
143
|
+
if (httpPort) {
|
|
144
|
+
try {
|
|
145
|
+
const httpServer = startHttpProxy(serverId, httpPort);
|
|
146
|
+
entry.httpServer = httpServer;
|
|
147
|
+
entry.httpPort = httpPort;
|
|
148
|
+
console.log(`[MCP] HTTP proxy for ${serverId} started on port ${httpPort}`);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// HTTP proxy failed, but stdio process is running - still usable
|
|
151
|
+
console.error(`[MCP] Failed to start HTTP proxy for ${serverId}:`, err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { success: true, port: httpPort };
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return { success: false, error: String(err) };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Start HTTP proxy server that forwards requests to stdio process
|
|
162
|
+
function startHttpProxy(serverId: string, port: number): Server {
|
|
163
|
+
return serve({
|
|
164
|
+
port,
|
|
165
|
+
async fetch(req) {
|
|
166
|
+
const url = new URL(req.url);
|
|
167
|
+
|
|
168
|
+
// CORS headers
|
|
169
|
+
const corsHeaders = {
|
|
170
|
+
"Access-Control-Allow-Origin": "*",
|
|
171
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
172
|
+
"Access-Control-Allow-Headers": "Content-Type, Mcp-Session-Id",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Handle CORS preflight
|
|
176
|
+
if (req.method === "OPTIONS") {
|
|
177
|
+
return new Response(null, { headers: corsHeaders });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Only accept POST to /mcp
|
|
181
|
+
if (req.method !== "POST" || (url.pathname !== "/mcp" && url.pathname !== "/")) {
|
|
182
|
+
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
183
|
+
status: 404,
|
|
184
|
+
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const body = await req.json() as JsonRpcRequest;
|
|
190
|
+
|
|
191
|
+
// Forward to stdio process with lock to serialize requests
|
|
192
|
+
const result = await withLock(serverId, async () => {
|
|
193
|
+
return await sendRequestRaw(serverId, body.method, body.params, body.id);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return new Response(JSON.stringify(result), {
|
|
197
|
+
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const errorResponse: JsonRpcResponse = {
|
|
201
|
+
jsonrpc: "2.0",
|
|
202
|
+
id: 0,
|
|
203
|
+
error: {
|
|
204
|
+
code: -32603,
|
|
205
|
+
message: err instanceof Error ? err.message : String(err),
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
return new Response(JSON.stringify(errorResponse), {
|
|
209
|
+
status: 500,
|
|
210
|
+
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Raw request that returns full JSON-RPC response (for proxy use)
|
|
218
|
+
async function sendRequestRaw(
|
|
219
|
+
serverId: string,
|
|
220
|
+
method: string,
|
|
221
|
+
params?: unknown,
|
|
222
|
+
requestId?: number
|
|
223
|
+
): Promise<JsonRpcResponse> {
|
|
224
|
+
const entry = mcpProcesses.get(serverId);
|
|
225
|
+
if (!entry) {
|
|
226
|
+
throw new Error("MCP process not running");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const id = requestId ?? ++entry.requestId;
|
|
230
|
+
const request: JsonRpcRequest = {
|
|
231
|
+
jsonrpc: "2.0",
|
|
232
|
+
id,
|
|
233
|
+
method,
|
|
234
|
+
params,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const requestLine = JSON.stringify(request) + "\n";
|
|
238
|
+
|
|
239
|
+
// Write to stdin
|
|
240
|
+
entry.proc.stdin.write(requestLine);
|
|
241
|
+
entry.proc.stdin.flush();
|
|
242
|
+
|
|
243
|
+
// Read response from stdout with timeout
|
|
244
|
+
return await readJsonRpcResponse(entry, id, 30000);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function stopMcpProcess(serverId: string): void {
|
|
248
|
+
const entry = mcpProcesses.get(serverId);
|
|
249
|
+
if (entry) {
|
|
250
|
+
// Stop HTTP proxy server first
|
|
251
|
+
if (entry.httpServer) {
|
|
252
|
+
try {
|
|
253
|
+
entry.httpServer.stop();
|
|
254
|
+
console.log(`[MCP] HTTP proxy for ${serverId} stopped`);
|
|
255
|
+
} catch {
|
|
256
|
+
// Ignore stop errors
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Kill the stdio process
|
|
260
|
+
try {
|
|
261
|
+
entry.proc.kill();
|
|
262
|
+
} catch {
|
|
263
|
+
// Ignore kill errors
|
|
264
|
+
}
|
|
265
|
+
mcpProcesses.delete(serverId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get the HTTP proxy URL for an MCP server
|
|
270
|
+
export function getMcpProxyUrl(serverId: string): string | null {
|
|
271
|
+
const entry = mcpProcesses.get(serverId);
|
|
272
|
+
if (entry?.httpPort) {
|
|
273
|
+
return `http://localhost:${entry.httpPort}/mcp`;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function sendRequest(serverId: string, method: string, params?: unknown): Promise<unknown> {
|
|
279
|
+
const entry = mcpProcesses.get(serverId);
|
|
280
|
+
if (!entry) {
|
|
281
|
+
throw new Error("MCP process not running");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const id = ++entry.requestId;
|
|
285
|
+
const request: JsonRpcRequest = {
|
|
286
|
+
jsonrpc: "2.0",
|
|
287
|
+
id,
|
|
288
|
+
method,
|
|
289
|
+
params,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const requestLine = JSON.stringify(request) + "\n";
|
|
293
|
+
|
|
294
|
+
// Write to stdin (Bun's FileSink has write() directly)
|
|
295
|
+
entry.proc.stdin.write(requestLine);
|
|
296
|
+
entry.proc.stdin.flush();
|
|
297
|
+
|
|
298
|
+
// Read response from stdout with timeout
|
|
299
|
+
const response = await readJsonRpcResponse(entry, id, 30000);
|
|
300
|
+
|
|
301
|
+
if (response.error) {
|
|
302
|
+
throw new Error(`MCP Error ${response.error.code}: ${response.error.message}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return response.result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function readJsonRpcResponse(
|
|
309
|
+
entry: McpProcess,
|
|
310
|
+
expectedId: number,
|
|
311
|
+
timeoutMs: number
|
|
312
|
+
): Promise<JsonRpcResponse> {
|
|
313
|
+
const decoder = new TextDecoder();
|
|
314
|
+
const startTime = Date.now();
|
|
315
|
+
|
|
316
|
+
// Initialize buffer if not exists
|
|
317
|
+
if (!entry.buffer) {
|
|
318
|
+
entry.buffer = "";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
322
|
+
// Check buffer first for complete lines
|
|
323
|
+
const lines = entry.buffer.split("\n");
|
|
324
|
+
entry.buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
325
|
+
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
if (!line.trim()) continue;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const response = JSON.parse(line) as JsonRpcResponse;
|
|
331
|
+
if (response.id === expectedId) {
|
|
332
|
+
return response;
|
|
333
|
+
}
|
|
334
|
+
// Ignore responses with different IDs (could be notifications)
|
|
335
|
+
} catch {
|
|
336
|
+
// Not valid JSON, continue
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Read more data from stdout
|
|
341
|
+
const reader = entry.proc.stdout.getReader();
|
|
342
|
+
try {
|
|
343
|
+
const { value, done } = await Promise.race([
|
|
344
|
+
reader.read(),
|
|
345
|
+
new Promise<{ value: undefined; done: true }>((resolve) =>
|
|
346
|
+
setTimeout(() => resolve({ value: undefined, done: true }), Math.min(1000, timeoutMs - (Date.now() - startTime)))
|
|
347
|
+
),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
if (value) {
|
|
351
|
+
entry.buffer += decoder.decode(value, { stream: true });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (done && !value) {
|
|
355
|
+
// Process might have exited
|
|
356
|
+
await new Promise(r => setTimeout(r, 100));
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
reader.releaseLock();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
throw new Error("Timeout waiting for MCP response");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function sendNotification(serverId: string, method: string, params?: unknown): Promise<void> {
|
|
367
|
+
const entry = mcpProcesses.get(serverId);
|
|
368
|
+
if (!entry) return;
|
|
369
|
+
|
|
370
|
+
const notification = {
|
|
371
|
+
jsonrpc: "2.0",
|
|
372
|
+
method,
|
|
373
|
+
params,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const notificationLine = JSON.stringify(notification) + "\n";
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
entry.proc.stdin.write(notificationLine);
|
|
380
|
+
entry.proc.stdin.flush();
|
|
381
|
+
} catch {
|
|
382
|
+
// Ignore notification errors
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function initializeMcpServer(serverId: string): Promise<{ name: string; version: string }> {
|
|
387
|
+
const entry = mcpProcesses.get(serverId);
|
|
388
|
+
if (!entry) {
|
|
389
|
+
throw new Error("MCP process not running");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (entry.initialized && entry.serverInfo) {
|
|
393
|
+
return entry.serverInfo;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const result = await sendRequest(serverId, "initialize", {
|
|
397
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
398
|
+
capabilities: {
|
|
399
|
+
roots: { listChanged: true },
|
|
400
|
+
},
|
|
401
|
+
clientInfo: {
|
|
402
|
+
name: "apteva",
|
|
403
|
+
version: "1.0.0",
|
|
404
|
+
},
|
|
405
|
+
}) as InitializeResult;
|
|
406
|
+
|
|
407
|
+
entry.serverInfo = result.serverInfo;
|
|
408
|
+
entry.initialized = true;
|
|
409
|
+
|
|
410
|
+
// Send initialized notification
|
|
411
|
+
await sendNotification(serverId, "notifications/initialized");
|
|
412
|
+
|
|
413
|
+
return entry.serverInfo;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function listMcpTools(serverId: string): Promise<McpTool[]> {
|
|
417
|
+
const entry = mcpProcesses.get(serverId);
|
|
418
|
+
if (!entry) {
|
|
419
|
+
throw new Error("MCP process not running");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!entry.initialized) {
|
|
423
|
+
await initializeMcpServer(serverId);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const result = await sendRequest(serverId, "tools/list") as ToolsListResult;
|
|
427
|
+
return result.tools || [];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function callMcpTool(
|
|
431
|
+
serverId: string,
|
|
432
|
+
toolName: string,
|
|
433
|
+
args: Record<string, unknown> = {}
|
|
434
|
+
): Promise<McpToolCallResult> {
|
|
435
|
+
const entry = mcpProcesses.get(serverId);
|
|
436
|
+
if (!entry) {
|
|
437
|
+
throw new Error("MCP process not running");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!entry.initialized) {
|
|
441
|
+
await initializeMcpServer(serverId);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const result = await sendRequest(serverId, "tools/call", {
|
|
445
|
+
name: toolName,
|
|
446
|
+
arguments: args,
|
|
447
|
+
}) as McpToolCallResult;
|
|
448
|
+
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============ HTTP MCP Client (for remote servers) ============
|
|
453
|
+
|
|
454
|
+
export class HttpMcpClient {
|
|
455
|
+
private url: string;
|
|
456
|
+
private headers: Record<string, string>;
|
|
457
|
+
private sessionId: string | null = null;
|
|
458
|
+
private initialized = false;
|
|
459
|
+
private requestId = 0;
|
|
460
|
+
private serverInfo: { name: string; version: string } | null = null;
|
|
461
|
+
|
|
462
|
+
constructor(url: string, headers: Record<string, string> = {}) {
|
|
463
|
+
this.url = url;
|
|
464
|
+
this.headers = headers;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private nextId(): number {
|
|
468
|
+
return ++this.requestId;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private async doRequest(method: string, params?: unknown): Promise<unknown> {
|
|
472
|
+
const request: JsonRpcRequest = {
|
|
473
|
+
jsonrpc: "2.0",
|
|
474
|
+
id: this.nextId(),
|
|
475
|
+
method,
|
|
476
|
+
params,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const headers: Record<string, string> = {
|
|
480
|
+
"Content-Type": "application/json",
|
|
481
|
+
"Accept": "application/json, text/event-stream",
|
|
482
|
+
...this.headers,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
if (this.sessionId) {
|
|
486
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const response = await fetch(this.url, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers,
|
|
492
|
+
body: JSON.stringify(request),
|
|
493
|
+
signal: AbortSignal.timeout(30000),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
if (!response.ok) {
|
|
497
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const newSessionId = response.headers.get("Mcp-Session-Id");
|
|
501
|
+
if (newSessionId) {
|
|
502
|
+
this.sessionId = newSessionId;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const contentType = response.headers.get("Content-Type") || "";
|
|
506
|
+
|
|
507
|
+
if (contentType.includes("text/event-stream")) {
|
|
508
|
+
return this.handleSSEResponse(response);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const data = await response.json() as JsonRpcResponse;
|
|
512
|
+
|
|
513
|
+
if (data.error) {
|
|
514
|
+
throw new Error(`MCP Error ${data.error.code}: ${data.error.message}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return data.result;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private async handleSSEResponse(response: Response): Promise<unknown> {
|
|
521
|
+
const text = await response.text();
|
|
522
|
+
const lines = text.split("\n");
|
|
523
|
+
|
|
524
|
+
for (const line of lines) {
|
|
525
|
+
if (line.startsWith("data: ")) {
|
|
526
|
+
const data = JSON.parse(line.slice(6)) as JsonRpcResponse;
|
|
527
|
+
if (data.error) {
|
|
528
|
+
throw new Error(`MCP Error ${data.error.code}: ${data.error.message}`);
|
|
529
|
+
}
|
|
530
|
+
return data.result;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
throw new Error("No data in SSE response");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async initialize(): Promise<{ name: string; version: string }> {
|
|
538
|
+
if (this.initialized && this.serverInfo) {
|
|
539
|
+
return this.serverInfo;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const result = await this.doRequest("initialize", {
|
|
543
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
544
|
+
capabilities: {
|
|
545
|
+
roots: { listChanged: true },
|
|
546
|
+
},
|
|
547
|
+
clientInfo: {
|
|
548
|
+
name: "apteva",
|
|
549
|
+
version: "1.0.0",
|
|
550
|
+
},
|
|
551
|
+
}) as InitializeResult;
|
|
552
|
+
|
|
553
|
+
this.serverInfo = result.serverInfo;
|
|
554
|
+
this.initialized = true;
|
|
555
|
+
|
|
556
|
+
return this.serverInfo;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async listTools(): Promise<McpTool[]> {
|
|
560
|
+
if (!this.initialized) {
|
|
561
|
+
await this.initialize();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const result = await this.doRequest("tools/list") as ToolsListResult;
|
|
565
|
+
return result.tools || [];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async callTool(name: string, args: Record<string, unknown> = {}): Promise<McpToolCallResult> {
|
|
569
|
+
if (!this.initialized) {
|
|
570
|
+
await this.initialize();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const result = await this.doRequest("tools/call", {
|
|
574
|
+
name,
|
|
575
|
+
arguments: args,
|
|
576
|
+
}) as McpToolCallResult;
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Cache for HTTP MCP clients
|
|
583
|
+
const httpClientCache = new Map<string, HttpMcpClient>();
|
|
584
|
+
|
|
585
|
+
export function getHttpMcpClient(url: string, headers: Record<string, string> = {}): HttpMcpClient {
|
|
586
|
+
const cacheKey = `${url}:${JSON.stringify(headers)}`;
|
|
587
|
+
|
|
588
|
+
let client = httpClientCache.get(cacheKey);
|
|
589
|
+
if (!client) {
|
|
590
|
+
client = new HttpMcpClient(url, headers);
|
|
591
|
+
httpClientCache.set(cacheKey, client);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return client;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function clearHttpMcpClientCache(): void {
|
|
598
|
+
httpClientCache.clear();
|
|
599
|
+
}
|
package/src/providers.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const PROVIDERS = {
|
|
|
7
7
|
id: "anthropic",
|
|
8
8
|
name: "Anthropic",
|
|
9
9
|
displayName: "Anthropic",
|
|
10
|
+
type: "llm" as const,
|
|
10
11
|
envVar: "ANTHROPIC_API_KEY",
|
|
11
12
|
docsUrl: "https://console.anthropic.com/settings/keys",
|
|
12
13
|
testEndpoint: "https://api.anthropic.com/v1/messages",
|
|
@@ -19,6 +20,7 @@ export const PROVIDERS = {
|
|
|
19
20
|
id: "openai",
|
|
20
21
|
name: "OpenAI",
|
|
21
22
|
displayName: "OpenAI",
|
|
23
|
+
type: "llm" as const,
|
|
22
24
|
envVar: "OPENAI_API_KEY",
|
|
23
25
|
docsUrl: "https://platform.openai.com/api-keys",
|
|
24
26
|
testEndpoint: "https://api.openai.com/v1/models",
|
|
@@ -31,6 +33,7 @@ export const PROVIDERS = {
|
|
|
31
33
|
id: "groq",
|
|
32
34
|
name: "Groq",
|
|
33
35
|
displayName: "Groq",
|
|
36
|
+
type: "llm" as const,
|
|
34
37
|
envVar: "GROQ_API_KEY",
|
|
35
38
|
docsUrl: "https://console.groq.com/keys",
|
|
36
39
|
testEndpoint: "https://api.groq.com/openai/v1/models",
|
|
@@ -43,6 +46,7 @@ export const PROVIDERS = {
|
|
|
43
46
|
id: "gemini",
|
|
44
47
|
name: "Google",
|
|
45
48
|
displayName: "Google Gemini",
|
|
49
|
+
type: "llm" as const,
|
|
46
50
|
envVar: "GEMINI_API_KEY",
|
|
47
51
|
docsUrl: "https://aistudio.google.com/app/apikey",
|
|
48
52
|
testEndpoint: "https://generativelanguage.googleapis.com/v1/models",
|
|
@@ -55,6 +59,7 @@ export const PROVIDERS = {
|
|
|
55
59
|
id: "xai",
|
|
56
60
|
name: "xAI",
|
|
57
61
|
displayName: "xAI Grok",
|
|
62
|
+
type: "llm" as const,
|
|
58
63
|
envVar: "XAI_API_KEY",
|
|
59
64
|
docsUrl: "https://console.x.ai/",
|
|
60
65
|
testEndpoint: "https://api.x.ai/v1/models",
|
|
@@ -67,6 +72,7 @@ export const PROVIDERS = {
|
|
|
67
72
|
id: "together",
|
|
68
73
|
name: "Together",
|
|
69
74
|
displayName: "Together AI",
|
|
75
|
+
type: "llm" as const,
|
|
70
76
|
envVar: "TOGETHER_API_KEY",
|
|
71
77
|
docsUrl: "https://api.together.xyz/settings/api-keys",
|
|
72
78
|
testEndpoint: "https://api.together.xyz/v1/models",
|
|
@@ -79,6 +85,7 @@ export const PROVIDERS = {
|
|
|
79
85
|
id: "fireworks",
|
|
80
86
|
name: "Fireworks",
|
|
81
87
|
displayName: "Fireworks AI",
|
|
88
|
+
type: "llm" as const,
|
|
82
89
|
envVar: "FIREWORKS_API_KEY",
|
|
83
90
|
docsUrl: "https://fireworks.ai/api-keys",
|
|
84
91
|
testEndpoint: "https://api.fireworks.ai/inference/v1/models",
|
|
@@ -91,6 +98,7 @@ export const PROVIDERS = {
|
|
|
91
98
|
id: "moonshot",
|
|
92
99
|
name: "Moonshot",
|
|
93
100
|
displayName: "Moonshot AI",
|
|
101
|
+
type: "llm" as const,
|
|
94
102
|
envVar: "MOONSHOT_API_KEY",
|
|
95
103
|
docsUrl: "https://platform.moonshot.cn/console/api-keys",
|
|
96
104
|
testEndpoint: "https://api.moonshot.cn/v1/models",
|
|
@@ -99,6 +107,27 @@ export const PROVIDERS = {
|
|
|
99
107
|
{ value: "moonshot-v1-32k", label: "Kimi 32K (Fast)" },
|
|
100
108
|
],
|
|
101
109
|
},
|
|
110
|
+
// MCP Integrations
|
|
111
|
+
composio: {
|
|
112
|
+
id: "composio",
|
|
113
|
+
name: "Composio",
|
|
114
|
+
displayName: "Composio",
|
|
115
|
+
type: "integration" as const,
|
|
116
|
+
envVar: "COMPOSIO_API_KEY",
|
|
117
|
+
docsUrl: "https://app.composio.dev/settings",
|
|
118
|
+
description: "500+ app integrations via MCP gateway",
|
|
119
|
+
models: [],
|
|
120
|
+
},
|
|
121
|
+
smithery: {
|
|
122
|
+
id: "smithery",
|
|
123
|
+
name: "Smithery",
|
|
124
|
+
displayName: "Smithery",
|
|
125
|
+
type: "integration" as const,
|
|
126
|
+
envVar: "SMITHERY_API_KEY",
|
|
127
|
+
docsUrl: "https://smithery.ai/settings",
|
|
128
|
+
description: "MCP server registry and hosting",
|
|
129
|
+
models: [],
|
|
130
|
+
},
|
|
102
131
|
} as const;
|
|
103
132
|
|
|
104
133
|
export type ProviderId = keyof typeof PROVIDERS;
|
|
@@ -228,7 +257,9 @@ export function getProvidersWithStatus() {
|
|
|
228
257
|
return Object.values(PROVIDERS).map(provider => ({
|
|
229
258
|
id: provider.id,
|
|
230
259
|
name: provider.displayName,
|
|
260
|
+
type: provider.type,
|
|
231
261
|
docsUrl: provider.docsUrl,
|
|
262
|
+
description: "description" in provider ? provider.description : undefined,
|
|
232
263
|
models: provider.models,
|
|
233
264
|
hasKey: configuredProviders.has(provider.id),
|
|
234
265
|
keyHint: keyStatuses.get(provider.id)?.key_hint || null,
|