apteva 0.4.57 → 0.7.1

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.
Files changed (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +15 -76
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2403
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. package/src/web/types.ts +0 -298
@@ -1,2515 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { McpIcon } from "../common/Icons";
3
- import { useAuth, useProjects } from "../../context";
4
- import { useConfirm, useAlert } from "../common/Modal";
5
- import { Select } from "../common/Select";
6
- import type { McpTool, McpToolCallResult } from "../../types";
7
- import { IntegrationsPanel } from "./IntegrationsPanel";
8
-
9
- interface McpServer {
10
- id: string;
11
- name: string;
12
- type: "npm" | "pip" | "github" | "http" | "custom";
13
- package: string | null;
14
- pip_module: string | null; // For pip type: module to run (e.g., "late.mcp")
15
- command: string | null;
16
- args: string | null;
17
- env: Record<string, string>;
18
- url: string | null;
19
- headers: Record<string, string>;
20
- port: number | null;
21
- status: "stopped" | "running";
22
- source: string | null; // "composio", "smithery", or null for local
23
- project_id: string | null; // null = global
24
- created_at: string;
25
- }
26
-
27
- interface RegistryServer {
28
- id: string;
29
- name: string;
30
- fullName: string;
31
- description: string;
32
- version?: string;
33
- repository?: string;
34
- npmPackage: string | null;
35
- remoteUrl: string | null;
36
- transport: string;
37
- }
38
-
39
- export function McpPage() {
40
- const { authFetch } = useAuth();
41
- const { projects, currentProjectId } = useProjects();
42
- const [servers, setServers] = useState<McpServer[]>([]);
43
- const [loading, setLoading] = useState(true);
44
- const [showAdd, setShowAdd] = useState(false);
45
- const [editingServer, setEditingServer] = useState<McpServer | null>(null);
46
- const [selectedServer, setSelectedServer] = useState<McpServer | null>(null);
47
- const [activeTab, setActiveTab] = useState<"servers" | "hosted" | "registry">("servers");
48
- const { confirm, ConfirmDialog } = useConfirm();
49
-
50
- const hasProjects = projects.length > 0;
51
-
52
- const fetchServers = async () => {
53
- try {
54
- const res = await authFetch("/api/mcp/servers");
55
- const data = await res.json();
56
- setServers(data.servers || []);
57
- } catch (e) {
58
- console.error("Failed to fetch MCP servers:", e);
59
- }
60
- setLoading(false);
61
- };
62
-
63
- useEffect(() => {
64
- fetchServers();
65
- }, [authFetch]);
66
-
67
- // Filter servers based on global project selector
68
- // When a project is selected, show global + that project's servers
69
- const filteredServers = servers.filter(server => {
70
- if (!currentProjectId) return true; // "All Projects" - show everything
71
- if (currentProjectId === "unassigned") return server.project_id === null; // Only global
72
- // Project selected: show global + project-specific
73
- return server.project_id === null || server.project_id === currentProjectId;
74
- });
75
-
76
- const startServer = async (id: string) => {
77
- try {
78
- await authFetch(`/api/mcp/servers/${id}/start`, { method: "POST" });
79
- fetchServers();
80
- } catch (e) {
81
- console.error("Failed to start server:", e);
82
- }
83
- };
84
-
85
- const stopServer = async (id: string) => {
86
- try {
87
- await authFetch(`/api/mcp/servers/${id}/stop`, { method: "POST" });
88
- fetchServers();
89
- } catch (e) {
90
- console.error("Failed to stop server:", e);
91
- }
92
- };
93
-
94
- const deleteServer = async (id: string) => {
95
- const confirmed = await confirm("Delete this MCP server?", { confirmText: "Delete", title: "Delete Server" });
96
- if (!confirmed) return;
97
- try {
98
- await authFetch(`/api/mcp/servers/${id}`, { method: "DELETE" });
99
- if (selectedServer?.id === id) {
100
- setSelectedServer(null);
101
- }
102
- fetchServers();
103
- } catch (e) {
104
- console.error("Failed to delete server:", e);
105
- }
106
- };
107
-
108
- const renameServer = async (id: string, newName: string) => {
109
- try {
110
- await authFetch(`/api/mcp/servers/${id}`, {
111
- method: "PUT",
112
- headers: { "Content-Type": "application/json" },
113
- body: JSON.stringify({ name: newName }),
114
- });
115
- fetchServers();
116
- } catch (e) {
117
- console.error("Failed to rename server:", e);
118
- }
119
- };
120
-
121
- const updateServer = async (id: string, updates: Partial<McpServer>) => {
122
- try {
123
- await authFetch(`/api/mcp/servers/${id}`, {
124
- method: "PUT",
125
- headers: { "Content-Type": "application/json" },
126
- body: JSON.stringify(updates),
127
- });
128
- fetchServers();
129
- } catch (e) {
130
- console.error("Failed to update server:", e);
131
- throw e;
132
- }
133
- };
134
-
135
- return (
136
- <>
137
- {ConfirmDialog}
138
- <div className="flex-1 overflow-auto p-6">
139
- <div className="max-w-6xl">
140
- {/* Header */}
141
- <div className="flex items-center justify-between mb-6">
142
- <div>
143
- <h1 className="text-2xl font-semibold mb-1">MCP Servers</h1>
144
- <p className="text-[var(--color-text-muted)]">
145
- Manage Model Context Protocol servers for tool integrations.
146
- </p>
147
- </div>
148
- {activeTab === "servers" && (
149
- <button
150
- onClick={() => setShowAdd(true)}
151
- className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black px-4 py-2 rounded font-medium transition"
152
- >
153
- + Add Server
154
- </button>
155
- )}
156
- </div>
157
-
158
- {/* Tabs */}
159
- <div className="flex gap-1 mb-6 bg-[var(--color-surface)] card p-1 w-fit">
160
- <button
161
- onClick={() => setActiveTab("servers")}
162
- className={`px-4 py-2 rounded text-sm font-medium transition ${
163
- activeTab === "servers"
164
- ? "bg-[var(--color-surface-raised)] text-white"
165
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
166
- }`}
167
- >
168
- My Servers
169
- </button>
170
- <button
171
- onClick={() => setActiveTab("hosted")}
172
- className={`px-4 py-2 rounded text-sm font-medium transition ${
173
- activeTab === "hosted"
174
- ? "bg-[var(--color-surface-raised)] text-white"
175
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
176
- }`}
177
- >
178
- Hosted Services
179
- </button>
180
- <button
181
- onClick={() => setActiveTab("registry")}
182
- className={`px-4 py-2 rounded text-sm font-medium transition ${
183
- activeTab === "registry"
184
- ? "bg-[var(--color-surface-raised)] text-white"
185
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
186
- }`}
187
- >
188
- Browse Registry
189
- </button>
190
- </div>
191
-
192
- {/* My Servers Tab */}
193
- {activeTab === "servers" && (
194
- <>
195
- {/* Loading */}
196
- {loading && (
197
- <div className="text-center py-8 text-[var(--color-text-muted)]">Loading...</div>
198
- )}
199
-
200
- {/* Empty State */}
201
- {!loading && filteredServers.length === 0 && servers.length === 0 && (
202
- <div className="bg-[var(--color-surface)] card p-8 text-center">
203
- <McpIcon className="w-12 h-12 text-[var(--color-border-light)] mx-auto mb-4" />
204
- <h3 className="text-lg font-medium mb-2">No MCP servers configured</h3>
205
- <p className="text-[var(--color-text-muted)] mb-6 max-w-md mx-auto">
206
- MCP servers extend your agents with tools like file access, web browsing,
207
- database connections, and more.
208
- </p>
209
- <div className="flex gap-3 justify-center">
210
- <button
211
- onClick={() => setShowAdd(true)}
212
- className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black px-4 py-2 rounded font-medium transition"
213
- >
214
- Add Manually
215
- </button>
216
- <button
217
- onClick={() => setActiveTab("registry")}
218
- className="border border-[var(--color-border-light)] hover:border-[var(--color-text-muted)] px-4 py-2 rounded font-medium transition"
219
- >
220
- Browse Registry
221
- </button>
222
- </div>
223
- </div>
224
- )}
225
-
226
- {/* Empty filter state */}
227
- {!loading && filteredServers.length === 0 && servers.length > 0 && (
228
- <div className="bg-[var(--color-surface)] card p-6 text-center">
229
- <p className="text-[var(--color-text-muted)]">No servers match this filter.</p>
230
- </div>
231
- )}
232
-
233
- {/* Main content with server list and tools panel */}
234
- {!loading && filteredServers.length > 0 && (
235
- <div className="flex gap-6">
236
- {/* Server List */}
237
- <div className={`space-y-3 ${selectedServer ? "w-1/2" : "w-full"}`}>
238
- {filteredServers.map(server => {
239
- const isRemote = server.type === "http" && server.url;
240
- const isAvailable = isRemote || server.status === "running";
241
- const project = hasProjects && server.project_id
242
- ? projects.find(p => p.id === server.project_id)
243
- : null;
244
- return (
245
- <McpServerCard
246
- key={server.id}
247
- server={server}
248
- project={project}
249
- selected={selectedServer?.id === server.id}
250
- onSelect={() => setSelectedServer(isAvailable ? server : null)}
251
- onStart={() => startServer(server.id)}
252
- onStop={() => stopServer(server.id)}
253
- onDelete={() => deleteServer(server.id)}
254
- onEdit={async () => {
255
- // Fetch full server details (with decrypted env/headers) for editing
256
- try {
257
- const res = await authFetch(`/api/mcp/servers/${server.id}`);
258
- const data = await res.json();
259
- setEditingServer(data.server || server);
260
- } catch {
261
- setEditingServer(server);
262
- }
263
- }}
264
- />
265
- );
266
- })}
267
- </div>
268
-
269
- {/* Tools Panel */}
270
- {selectedServer && (
271
- <div className="w-1/2">
272
- <ToolsPanel
273
- server={selectedServer}
274
- onClose={() => setSelectedServer(null)}
275
- />
276
- </div>
277
- )}
278
- </div>
279
- )}
280
- </>
281
- )}
282
-
283
- {/* Hosted Services Tab */}
284
- {activeTab === "hosted" && (
285
- <HostedServices onServerAdded={fetchServers} projectId={currentProjectId} />
286
- )}
287
-
288
- {/* Browse Registry Tab */}
289
- {activeTab === "registry" && (
290
- <RegistryBrowser
291
- onInstall={(server) => {
292
- // After installing, switch to servers tab and refresh
293
- fetchServers();
294
- setActiveTab("servers");
295
- }}
296
- />
297
- )}
298
-
299
- {/* Info - only show on servers tab */}
300
- {activeTab === "servers" && (
301
- <div className="mt-8 p-4 bg-[var(--color-surface)] card">
302
- <h3 className="font-medium mb-2">Quick Start</h3>
303
- <p className="text-sm text-[var(--color-text-muted)] mb-3">
304
- Add an MCP server by providing its npm package name. For example:
305
- </p>
306
- <div className="flex flex-wrap gap-2">
307
- {[
308
- { name: "filesystem", pkg: "@modelcontextprotocol/server-filesystem" },
309
- { name: "fetch", pkg: "@modelcontextprotocol/server-fetch" },
310
- { name: "memory", pkg: "@modelcontextprotocol/server-memory" },
311
- ].map(s => (
312
- <code key={s.name} className="text-xs bg-[var(--color-bg)] px-2 py-1 rounded">
313
- {s.pkg}
314
- </code>
315
- ))}
316
- </div>
317
- </div>
318
- )}
319
- </div>
320
-
321
- {/* Add Server Modal */}
322
- {showAdd && (
323
- <AddServerModal
324
- onClose={() => setShowAdd(false)}
325
- onAdded={() => {
326
- setShowAdd(false);
327
- fetchServers();
328
- }}
329
- projects={hasProjects ? projects : undefined}
330
- defaultProjectId={currentProjectId && currentProjectId !== "unassigned" ? currentProjectId : null}
331
- />
332
- )}
333
-
334
- {editingServer && (
335
- <EditServerModal
336
- server={editingServer}
337
- projects={hasProjects ? projects : undefined}
338
- onClose={() => setEditingServer(null)}
339
- onSaved={() => {
340
- setEditingServer(null);
341
- fetchServers();
342
- }}
343
- />
344
- )}
345
- </div>
346
- </>
347
- );
348
- }
349
-
350
- function McpServerCard({
351
- server,
352
- project,
353
- selected,
354
- onSelect,
355
- onStart,
356
- onStop,
357
- onDelete,
358
- onEdit,
359
- }: {
360
- server: McpServer;
361
- project?: { id: string; name: string; color: string } | null;
362
- selected: boolean;
363
- onSelect: () => void;
364
- onStart: () => void;
365
- onStop: () => void;
366
- onDelete: () => void;
367
- onEdit: () => void;
368
- }) {
369
- // Remote/hosted servers (http type with url) are always available
370
- const isRemote = server.type === "http" && server.url;
371
- const isAvailable = isRemote || server.status === "running";
372
-
373
- // Determine what to show as the server info
374
- const getServerInfo = () => {
375
- if (isRemote) {
376
- // Show source (composio, smithery) or just "remote"
377
- const source = server.source || "remote";
378
- return `${source} • http`;
379
- }
380
- return `${server.type} • ${server.package || server.command || "custom"}${
381
- server.status === "running" && server.port ? ` • :${server.port}` : ""
382
- }`;
383
- };
384
-
385
- // Scope badge: Global or Project name
386
- const getScopeBadge = () => {
387
- if (project) {
388
- return (
389
- <span
390
- className="text-xs px-1.5 py-0.5 rounded"
391
- style={{ backgroundColor: `${project.color}20`, color: project.color }}
392
- >
393
- {project.name}
394
- </span>
395
- );
396
- }
397
- if (server.project_id === null) {
398
- return (
399
- <span className="text-xs text-[var(--color-text-muted)] bg-[var(--color-surface-raised)] px-1.5 py-0.5 rounded">
400
- Global
401
- </span>
402
- );
403
- }
404
- return null;
405
- };
406
-
407
- return (
408
- <div
409
- className={`bg-[var(--color-surface)] border rounded-lg p-4 cursor-pointer transition ${
410
- selected ? "border-[var(--color-accent)]" : "border-[var(--color-border)] hover:border-[var(--color-border-light)]"
411
- }`}
412
- onClick={isAvailable ? onSelect : undefined}
413
- >
414
- <div className="flex items-center justify-between">
415
- <div className="flex items-center gap-3">
416
- <div className={`w-2 h-2 rounded-full ${
417
- isAvailable ? "bg-green-400" : "bg-[var(--color-scrollbar)]"
418
- }`} />
419
- <div>
420
- <div className="flex items-center gap-2">
421
- <h3 className="font-medium">{server.name}</h3>
422
- {getScopeBadge()}
423
- </div>
424
- <p className="text-sm text-[var(--color-text-muted)]">{getServerInfo()}</p>
425
- </div>
426
- </div>
427
- <div className="flex items-center gap-2">
428
- <button
429
- onClick={(e) => { e.stopPropagation(); onEdit(); }}
430
- className="text-sm text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] px-3 py-1 transition"
431
- title="Edit server settings"
432
- >
433
- Edit
434
- </button>
435
- {isRemote ? (
436
- // Remote servers: no start/stop, just delete
437
- <button
438
- onClick={(e) => { e.stopPropagation(); onDelete(); }}
439
- className="text-sm text-[var(--color-text-muted)] hover:text-red-400 px-3 py-1 transition"
440
- >
441
- Remove
442
- </button>
443
- ) : server.status === "running" ? (
444
- // Local running server: tools + stop + delete
445
- <>
446
- <button
447
- onClick={(e) => { e.stopPropagation(); onSelect(); }}
448
- className="text-sm text-[var(--color-accent)] hover:text-[var(--color-accent-hover)] px-3 py-1 transition"
449
- >
450
- Tools
451
- </button>
452
- <button
453
- onClick={(e) => { e.stopPropagation(); onStop(); }}
454
- className="text-sm text-[var(--color-text-muted)] hover:text-red-400 px-3 py-1 transition"
455
- >
456
- Stop
457
- </button>
458
- <button
459
- onClick={(e) => { e.stopPropagation(); onDelete(); }}
460
- className="text-sm text-[var(--color-text-muted)] hover:text-red-400 px-3 py-1 transition"
461
- >
462
- Delete
463
- </button>
464
- </>
465
- ) : (
466
- // Local stopped server: start + delete
467
- <>
468
- <button
469
- onClick={(e) => { e.stopPropagation(); onStart(); }}
470
- className="text-sm text-[var(--color-text-muted)] hover:text-green-400 px-3 py-1 transition"
471
- >
472
- Start
473
- </button>
474
- <button
475
- onClick={(e) => { e.stopPropagation(); onDelete(); }}
476
- className="text-sm text-[var(--color-text-muted)] hover:text-red-400 px-3 py-1 transition"
477
- >
478
- Delete
479
- </button>
480
- </>
481
- )}
482
- </div>
483
- </div>
484
- </div>
485
- );
486
- }
487
-
488
- function ToolsPanel({
489
- server,
490
- onClose,
491
- }: {
492
- server: McpServer;
493
- onClose: () => void;
494
- }) {
495
- const { authFetch } = useAuth();
496
- const [tools, setTools] = useState<McpTool[]>([]);
497
- const [serverInfo, setServerInfo] = useState<{ name: string; version: string } | null>(null);
498
- const [loading, setLoading] = useState(true);
499
- const [error, setError] = useState<string | null>(null);
500
- const [selectedTool, setSelectedTool] = useState<McpTool | null>(null);
501
-
502
- useEffect(() => {
503
- const fetchTools = async () => {
504
- setLoading(true);
505
- setError(null);
506
- try {
507
- const res = await authFetch(`/api/mcp/servers/${server.id}/tools`);
508
- const data = await res.json();
509
- if (!res.ok) {
510
- setError(data.error || "Failed to fetch tools");
511
- return;
512
- }
513
- setTools(data.tools || []);
514
- setServerInfo(data.serverInfo || null);
515
- } catch (e) {
516
- setError(`Failed to fetch tools: ${e}`);
517
- } finally {
518
- setLoading(false);
519
- }
520
- };
521
-
522
- fetchTools();
523
- }, [server.id, authFetch]);
524
-
525
- return (
526
- <div className="bg-[var(--color-surface)] card overflow-hidden">
527
- {/* Header */}
528
- <div className="p-4 border-b border-[var(--color-border)] flex items-center justify-between">
529
- <div>
530
- <h3 className="font-medium">{server.name} Tools</h3>
531
- {serverInfo && (
532
- <p className="text-xs text-[var(--color-text-muted)]">
533
- {serverInfo.name} v{serverInfo.version}
534
- </p>
535
- )}
536
- </div>
537
- <button
538
- onClick={onClose}
539
- className="text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] text-xl leading-none"
540
- >
541
- ×
542
- </button>
543
- </div>
544
-
545
- {/* Content */}
546
- <div className="p-4 max-h-[500px] overflow-auto">
547
- {loading && <p className="text-[var(--color-text-muted)]">Loading tools...</p>}
548
-
549
- {error && (
550
- <div className="text-red-400 text-sm p-3 bg-red-500/10 rounded">
551
- {error}
552
- </div>
553
- )}
554
-
555
- {!loading && !error && tools.length === 0 && (
556
- <p className="text-[var(--color-text-muted)]">No tools available from this server.</p>
557
- )}
558
-
559
- {!loading && !error && tools.length > 0 && !selectedTool && (
560
- <div className="space-y-2">
561
- {tools.map(tool => (
562
- <button
563
- key={tool.name}
564
- onClick={() => setSelectedTool(tool)}
565
- className="w-full text-left p-3 bg-[var(--color-bg)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] hover:border-[var(--color-border-light)] rounded transition"
566
- >
567
- <div className="font-medium text-sm">{tool.name}</div>
568
- {tool.description && (
569
- <div className="text-xs text-[var(--color-text-muted)] mt-1">{tool.description}</div>
570
- )}
571
- </button>
572
- ))}
573
- </div>
574
- )}
575
-
576
- {selectedTool && (
577
- <ToolTester
578
- serverId={server.id}
579
- tool={selectedTool}
580
- onBack={() => setSelectedTool(null)}
581
- />
582
- )}
583
- </div>
584
- </div>
585
- );
586
- }
587
-
588
- function ToolTester({
589
- serverId,
590
- tool,
591
- onBack,
592
- }: {
593
- serverId: string;
594
- tool: McpTool;
595
- onBack: () => void;
596
- }) {
597
- const { authFetch } = useAuth();
598
- const [args, setArgs] = useState<string>("{}");
599
- const [result, setResult] = useState<McpToolCallResult | null>(null);
600
- const [error, setError] = useState<string | null>(null);
601
- const [loading, setLoading] = useState(false);
602
-
603
- // Generate default args from schema
604
- useEffect(() => {
605
- const schema = tool.inputSchema;
606
- if (schema && typeof schema === "object" && "properties" in schema) {
607
- const properties = schema.properties as Record<string, { type?: string; default?: unknown }>;
608
- const defaultArgs: Record<string, unknown> = {};
609
- for (const [key, prop] of Object.entries(properties)) {
610
- if (prop.default !== undefined) {
611
- defaultArgs[key] = prop.default;
612
- } else if (prop.type === "string") {
613
- defaultArgs[key] = "";
614
- } else if (prop.type === "number" || prop.type === "integer") {
615
- defaultArgs[key] = 0;
616
- } else if (prop.type === "boolean") {
617
- defaultArgs[key] = false;
618
- } else if (prop.type === "array") {
619
- defaultArgs[key] = [];
620
- } else if (prop.type === "object") {
621
- defaultArgs[key] = {};
622
- }
623
- }
624
- setArgs(JSON.stringify(defaultArgs, null, 2));
625
- }
626
- }, [tool]);
627
-
628
- const callTool = async () => {
629
- setLoading(true);
630
- setError(null);
631
- setResult(null);
632
-
633
- try {
634
- const parsedArgs = JSON.parse(args);
635
- const res = await authFetch(`/api/mcp/servers/${serverId}/tools/${encodeURIComponent(tool.name)}/call`, {
636
- method: "POST",
637
- headers: { "Content-Type": "application/json" },
638
- body: JSON.stringify({ arguments: parsedArgs }),
639
- });
640
- const data = await res.json();
641
-
642
- if (!res.ok) {
643
- setError(data.error || "Failed to call tool");
644
- return;
645
- }
646
-
647
- setResult(data.result);
648
- } catch (e) {
649
- setError(`Error: ${e}`);
650
- } finally {
651
- setLoading(false);
652
- }
653
- };
654
-
655
- return (
656
- <div className="space-y-4">
657
- {/* Header */}
658
- <div className="flex items-center gap-2">
659
- <button
660
- onClick={onBack}
661
- className="text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] text-sm"
662
- >
663
- ← Back
664
- </button>
665
- <span className="text-[var(--color-text-faint)]">/</span>
666
- <span className="font-medium">{tool.name}</span>
667
- </div>
668
-
669
- {/* Description */}
670
- {tool.description && (
671
- <p className="text-sm text-[var(--color-text-muted)]">{tool.description}</p>
672
- )}
673
-
674
- {/* Schema info */}
675
- {tool.inputSchema && (
676
- <div className="text-xs">
677
- <details className="cursor-pointer">
678
- <summary className="text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]">Input Schema</summary>
679
- <pre className="mt-2 p-2 bg-[var(--color-bg)] rounded text-[var(--color-text-secondary)] overflow-auto max-h-32">
680
- {JSON.stringify(tool.inputSchema, null, 2)}
681
- </pre>
682
- </details>
683
- </div>
684
- )}
685
-
686
- {/* Arguments input */}
687
- <div>
688
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Arguments (JSON)</label>
689
- <textarea
690
- value={args}
691
- onChange={(e) => setArgs(e.target.value)}
692
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 h-32 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)] resize-none"
693
- placeholder="{}"
694
- />
695
- </div>
696
-
697
- {/* Call button */}
698
- <button
699
- onClick={callTool}
700
- disabled={loading}
701
- className="w-full bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
702
- >
703
- {loading ? "Calling..." : "Call Tool"}
704
- </button>
705
-
706
- {/* Error */}
707
- {error && (
708
- <div className="text-red-400 text-sm p-3 bg-red-500/10 rounded">
709
- {error}
710
- </div>
711
- )}
712
-
713
- {/* Result */}
714
- {result && (
715
- <div className="space-y-2">
716
- <div className="text-sm text-[var(--color-text-muted)]">
717
- Result {result.isError && <span className="text-red-400">(error)</span>}
718
- </div>
719
- <div className={`p-3 rounded text-sm ${result.isError ? "bg-red-500/10" : "bg-green-500/10"}`}>
720
- {result.content.map((block, i) => (
721
- <div key={i} className="mb-2 last:mb-0">
722
- {block.type === "text" && (
723
- <pre className="whitespace-pre-wrap font-mono text-xs">
724
- {block.text}
725
- </pre>
726
- )}
727
- {block.type === "image" && block.data && (
728
- <img
729
- src={`data:${block.mimeType || "image/png"};base64,${block.data}`}
730
- alt="Tool result"
731
- className="max-w-full rounded"
732
- />
733
- )}
734
- </div>
735
- ))}
736
- </div>
737
- </div>
738
- )}
739
- </div>
740
- );
741
- }
742
-
743
- function RegistryBrowser({
744
- onInstall,
745
- }: {
746
- onInstall: (server: RegistryServer) => void;
747
- }) {
748
- const { authFetch } = useAuth();
749
- const [search, setSearch] = useState("");
750
- const [servers, setServers] = useState<RegistryServer[]>([]);
751
- const [loading, setLoading] = useState(false);
752
- const [searched, setSearched] = useState(false);
753
- const [installing, setInstalling] = useState<string | null>(null);
754
- const [error, setError] = useState<string | null>(null);
755
-
756
- const searchRegistry = async (query: string) => {
757
- setLoading(true);
758
- setError(null);
759
- try {
760
- const res = await authFetch(`/api/mcp/registry?search=${encodeURIComponent(query)}&limit=20`);
761
- const data = await res.json();
762
- if (!res.ok) {
763
- setError(data.error || "Failed to search registry");
764
- setServers([]);
765
- } else {
766
- setServers(data.servers || []);
767
- }
768
- } catch (e) {
769
- setError(`Failed to search: ${e}`);
770
- setServers([]);
771
- } finally {
772
- setLoading(false);
773
- setSearched(true);
774
- }
775
- };
776
-
777
- const handleSearch = (e: React.FormEvent) => {
778
- e.preventDefault();
779
- if (search.trim()) {
780
- searchRegistry(search.trim());
781
- }
782
- };
783
-
784
- // Load popular servers on mount
785
- useEffect(() => {
786
- searchRegistry("");
787
- }, []);
788
-
789
- const installServer = async (server: RegistryServer) => {
790
- if (!server.npmPackage) {
791
- setError("This server does not have an npm package");
792
- return;
793
- }
794
-
795
- setInstalling(server.id);
796
- setError(null);
797
-
798
- try {
799
- const res = await authFetch("/api/mcp/servers", {
800
- method: "POST",
801
- headers: { "Content-Type": "application/json" },
802
- body: JSON.stringify({
803
- name: server.name,
804
- type: "npm",
805
- package: server.npmPackage,
806
- }),
807
- });
808
-
809
- if (!res.ok) {
810
- const data = await res.json();
811
- setError(data.error || "Failed to add server");
812
- return;
813
- }
814
-
815
- onInstall(server);
816
- } catch (e) {
817
- setError(`Failed to add server: ${e}`);
818
- } finally {
819
- setInstalling(null);
820
- }
821
- };
822
-
823
- return (
824
- <div className="space-y-6">
825
- {/* Search */}
826
- <form onSubmit={handleSearch} className="flex gap-2">
827
- <input
828
- type="text"
829
- value={search}
830
- onChange={(e) => setSearch(e.target.value)}
831
- placeholder="Search MCP servers (e.g., filesystem, github, slack...)"
832
- className="flex-1 bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded-lg px-4 py-3 focus:outline-none focus:border-[var(--color-accent)]"
833
- />
834
- <button
835
- type="submit"
836
- disabled={loading}
837
- className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-6 py-3 rounded-lg font-medium transition"
838
- >
839
- {loading ? "..." : "Search"}
840
- </button>
841
- </form>
842
-
843
- {/* Error */}
844
- {error && (
845
- <div className="text-red-400 text-sm p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
846
- {error}
847
- </div>
848
- )}
849
-
850
- {/* Results */}
851
- {!loading && searched && servers.length === 0 && (
852
- <div className="text-center py-8 text-[var(--color-text-muted)]">
853
- No servers found. Try a different search term.
854
- </div>
855
- )}
856
-
857
- {servers.length > 0 && (
858
- <div className="grid gap-4 md:grid-cols-2">
859
- {servers.map((server) => (
860
- <div
861
- key={server.id}
862
- className="bg-[var(--color-surface)] card p-4 hover:border-[var(--color-border-light)] transition"
863
- >
864
- <div className="flex items-start justify-between gap-3">
865
- <div className="flex-1 min-w-0">
866
- <h3 className="font-medium truncate">{server.name}</h3>
867
- <p className="text-sm text-[var(--color-text-muted)] mt-1 line-clamp-2">
868
- {server.description || "No description"}
869
- </p>
870
- <div className="flex items-center gap-2 mt-2 text-xs text-[var(--color-text-faint)]">
871
- {server.version && <span>v{server.version}</span>}
872
- <span className={`px-1.5 py-0.5 rounded ${
873
- server.npmPackage ? "bg-green-500/10 text-green-400" : "bg-blue-500/10 text-blue-400"
874
- }`}>
875
- {server.npmPackage ? "npm" : "remote"}
876
- </span>
877
- </div>
878
- <code className="text-xs text-[var(--color-text-faint)] bg-[var(--color-bg)] px-2 py-0.5 rounded mt-2 inline-block truncate max-w-full">
879
- {server.npmPackage || server.fullName}
880
- </code>
881
- </div>
882
- <div className="flex-shrink-0">
883
- {server.npmPackage ? (
884
- <button
885
- onClick={() => installServer(server)}
886
- disabled={installing === server.id}
887
- className="text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] hover:border-[var(--color-accent)] px-3 py-1.5 rounded transition disabled:opacity-50"
888
- >
889
- {installing === server.id ? "Adding..." : "Add"}
890
- </button>
891
- ) : server.repository ? (
892
- <a
893
- href={server.repository}
894
- target="_blank"
895
- rel="noopener noreferrer"
896
- className="text-sm text-[var(--color-text-muted)] hover:text-[var(--color-accent)] transition"
897
- >
898
- View →
899
- </a>
900
- ) : null}
901
- </div>
902
- </div>
903
- </div>
904
- ))}
905
- </div>
906
- )}
907
-
908
- {/* Loading */}
909
- {loading && (
910
- <div className="text-center py-8 text-[var(--color-text-muted)]">
911
- Searching registry...
912
- </div>
913
- )}
914
-
915
- {/* Registry info */}
916
- <div className="p-4 bg-[var(--color-surface)] card text-sm text-[var(--color-text-muted)]">
917
- <p>
918
- Servers are sourced from the{" "}
919
- <a
920
- href="https://github.com/modelcontextprotocol/servers"
921
- target="_blank"
922
- rel="noopener noreferrer"
923
- className="text-[var(--color-accent)] hover:underline"
924
- >
925
- official MCP registry
926
- </a>
927
- . Not all servers have npm packages - some require manual setup.
928
- </p>
929
- </div>
930
- </div>
931
- );
932
- }
933
-
934
- // Hosted MCP Services (Composio, Smithery, etc.)
935
- interface ComposioConfig {
936
- id: string;
937
- name: string;
938
- toolkits: string[];
939
- toolsCount: number;
940
- mcpUrl: string;
941
- createdAt?: string;
942
- }
943
-
944
- function HostedServices({ onServerAdded, projectId }: { onServerAdded?: () => void; projectId?: string | null }) {
945
- const { authFetch } = useAuth();
946
- const [activeProvider, setActiveProvider] = useState<"composio" | "smithery" | "agentdojo">("composio");
947
- const [subTab, setSubTab] = useState<"configs" | "connect">("configs");
948
- const [composioConnected, setComposioConnected] = useState(false);
949
- const [smitheryConnected, setSmitheryConnected] = useState(false);
950
- const [agentDojoConnected, setAgentDojoConnected] = useState(false);
951
- const [composioConfigs, setComposioConfigs] = useState<ComposioConfig[]>([]);
952
- const [addedServers, setAddedServers] = useState<Set<string>>(new Set());
953
- const [loading, setLoading] = useState(true);
954
- const [loadingConfigs, setLoadingConfigs] = useState(false);
955
- const [addingConfig, setAddingConfig] = useState<string | null>(null);
956
- const { alert, AlertDialog } = useAlert();
957
-
958
- const fetchStatus = async () => {
959
- try {
960
- const serversUrl = projectId && projectId !== "unassigned"
961
- ? `/api/mcp/servers?project=${encodeURIComponent(projectId)}`
962
- : "/api/mcp/servers";
963
- const [providersRes, serversRes] = await Promise.all([
964
- authFetch("/api/providers"),
965
- authFetch(serversUrl),
966
- ]);
967
- const providersData = await providersRes.json();
968
- const serversData = await serversRes.json();
969
-
970
- const providers = providersData.providers || [];
971
- const servers = serversData.servers || [];
972
-
973
- // Track which Composio config IDs are already added as servers
974
- // Extract config ID from URLs like https://backend.composio.dev/v3/mcp/{configId}/mcp?user_id=...
975
- const composioConfigIds = new Set(
976
- servers
977
- .filter((s: any) => s.source === "composio" && s.url)
978
- .map((s: any) => {
979
- const match = s.url.match(/\/v3\/mcp\/([^/]+)/);
980
- return match ? match[1] : null;
981
- })
982
- .filter(Boolean)
983
- );
984
- setAddedServers(composioConfigIds);
985
-
986
- const composio = providers.find((p: any) => p.id === "composio");
987
- const smithery = providers.find((p: any) => p.id === "smithery");
988
- const agentdojo = providers.find((p: any) => p.id === "agentdojo");
989
- const composioHasKey = composio?.hasKey || false;
990
- const smitheryHasKey = smithery?.hasKey || false;
991
- const agentdojoHasKey = agentdojo?.hasKey || false;
992
-
993
- setComposioConnected(composioHasKey);
994
- setSmitheryConnected(smitheryHasKey);
995
- setAgentDojoConnected(agentdojoHasKey);
996
-
997
- // Set initial active provider to first connected one
998
- if (composioHasKey) {
999
- setActiveProvider("composio");
1000
- fetchComposioConfigs();
1001
- } else if (smitheryHasKey) {
1002
- setActiveProvider("smithery");
1003
- } else if (agentdojoHasKey) {
1004
- setActiveProvider("agentdojo");
1005
- }
1006
- } catch (e) {
1007
- console.error("Failed to fetch providers:", e);
1008
- }
1009
- setLoading(false);
1010
- };
1011
-
1012
- const fetchComposioConfigs = async () => {
1013
- setLoadingConfigs(true);
1014
- try {
1015
- const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
1016
- const res = await authFetch(`/api/integrations/composio/configs${projectParam}`);
1017
- const data = await res.json();
1018
- setComposioConfigs(data.configs || []);
1019
- } catch (e) {
1020
- console.error("Failed to fetch Composio configs:", e);
1021
- }
1022
- setLoadingConfigs(false);
1023
- };
1024
-
1025
- const addComposioConfig = async (configId: string) => {
1026
- setAddingConfig(configId);
1027
- try {
1028
- const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
1029
- const res = await authFetch(`/api/integrations/composio/configs/${configId}/add${projectParam}`, {
1030
- method: "POST",
1031
- });
1032
- if (res.ok) {
1033
- // Mark as added by config ID
1034
- setAddedServers(prev => new Set([...prev, configId]));
1035
- onServerAdded?.();
1036
- } else {
1037
- const data = await res.json();
1038
- await alert(data.error || "Failed to add config", { title: "Error", variant: "error" });
1039
- }
1040
- } catch (e) {
1041
- console.error("Failed to add config:", e);
1042
- }
1043
- setAddingConfig(null);
1044
- };
1045
-
1046
- const isConfigAdded = (configId: string) => {
1047
- return addedServers.has(configId);
1048
- };
1049
-
1050
- useEffect(() => {
1051
- fetchStatus();
1052
- }, [authFetch, projectId]);
1053
-
1054
- if (loading) {
1055
- return <div className="text-center py-8 text-[var(--color-text-muted)]">Loading...</div>;
1056
- }
1057
-
1058
- const hasAnyConnection = composioConnected || smitheryConnected || agentDojoConnected;
1059
- const connectedCount = [composioConnected, smitheryConnected, agentDojoConnected].filter(Boolean).length;
1060
-
1061
- if (!hasAnyConnection) {
1062
- return (
1063
- <div className="bg-[var(--color-surface)] card p-8 text-center">
1064
- <p className="text-[var(--color-text-secondary)] mb-2">No hosted MCP services connected</p>
1065
- <p className="text-sm text-[var(--color-text-muted)] mb-4">
1066
- Connect Composio, Smithery, or AgentDojo in Settings to access cloud-based MCP servers.
1067
- </p>
1068
- <a
1069
- href="/settings"
1070
- className="inline-block bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] hover:border-[var(--color-accent)] px-4 py-2 rounded text-sm font-medium transition"
1071
- >
1072
- Go to Settings →
1073
- </a>
1074
- </div>
1075
- );
1076
- }
1077
-
1078
- return (
1079
- <>
1080
- {AlertDialog}
1081
- <div className="space-y-6">
1082
- {/* Provider Tabs - show when multiple providers are connected */}
1083
- {connectedCount > 1 && (
1084
- <div className="flex gap-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded-lg p-1 w-fit">
1085
- {composioConnected && (
1086
- <button
1087
- onClick={() => { setActiveProvider("composio"); setSubTab("configs"); }}
1088
- className={`px-4 py-2 rounded text-sm font-medium transition flex items-center gap-2 ${
1089
- activeProvider === "composio"
1090
- ? "bg-[var(--color-surface-raised)] text-white"
1091
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1092
- }`}
1093
- >
1094
- <span className="w-2 h-2 rounded-full bg-purple-500" />
1095
- Composio
1096
- </button>
1097
- )}
1098
- {smitheryConnected && (
1099
- <button
1100
- onClick={() => setActiveProvider("smithery")}
1101
- className={`px-4 py-2 rounded text-sm font-medium transition flex items-center gap-2 ${
1102
- activeProvider === "smithery"
1103
- ? "bg-[var(--color-surface-raised)] text-white"
1104
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1105
- }`}
1106
- >
1107
- <span className="w-2 h-2 rounded-full bg-blue-500" />
1108
- Smithery
1109
- </button>
1110
- )}
1111
- {agentDojoConnected && (
1112
- <button
1113
- onClick={() => setActiveProvider("agentdojo")}
1114
- className={`px-4 py-2 rounded text-sm font-medium transition flex items-center gap-2 ${
1115
- activeProvider === "agentdojo"
1116
- ? "bg-[var(--color-surface-raised)] text-white"
1117
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1118
- }`}
1119
- >
1120
- <span className="w-2 h-2 rounded-full bg-green-500" />
1121
- AgentDojo
1122
- </button>
1123
- )}
1124
- </div>
1125
- )}
1126
-
1127
- {/* Composio Content */}
1128
- {composioConnected && (connectedCount === 1 || activeProvider === "composio") && (
1129
- <>
1130
- {/* Sub-tabs for Composio */}
1131
- <div className="flex items-center justify-between">
1132
- <div className="flex gap-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded-lg p-1">
1133
- <button
1134
- onClick={() => setSubTab("configs")}
1135
- className={`px-4 py-2 rounded text-sm font-medium transition ${
1136
- subTab === "configs"
1137
- ? "bg-[var(--color-surface-raised)] text-white"
1138
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1139
- }`}
1140
- >
1141
- MCP Configs
1142
- </button>
1143
- <button
1144
- onClick={() => setSubTab("connect")}
1145
- className={`px-4 py-2 rounded text-sm font-medium transition ${
1146
- subTab === "connect"
1147
- ? "bg-[var(--color-surface-raised)] text-white"
1148
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1149
- }`}
1150
- >
1151
- Connect Apps
1152
- </button>
1153
- </div>
1154
- {connectedCount === 1 && (
1155
- <div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
1156
- <span className="w-2 h-2 rounded-full bg-purple-500" />
1157
- Composio
1158
- <span className="text-green-400">Connected</span>
1159
- </div>
1160
- )}
1161
- </div>
1162
-
1163
- {/* Connect Apps Tab */}
1164
- {subTab === "connect" && (
1165
- <div>
1166
- <p className="text-sm text-[var(--color-text-muted)] mb-4">
1167
- Connect your accounts to enable tools in MCP configs
1168
- </p>
1169
- <IntegrationsPanel
1170
- providerId="composio"
1171
- projectId={projectId}
1172
- onConnectionComplete={() => {
1173
- // Refresh configs after connecting an app
1174
- fetchComposioConfigs();
1175
- }}
1176
- />
1177
- </div>
1178
- )}
1179
-
1180
- {/* MCP Configs Tab */}
1181
- {subTab === "configs" && (
1182
- <div>
1183
- <div className="flex items-center justify-between mb-3">
1184
- <p className="text-sm text-[var(--color-text-muted)]">
1185
- Your MCP configs from Composio
1186
- </p>
1187
- <div className="flex items-center gap-3">
1188
- <button
1189
- onClick={fetchComposioConfigs}
1190
- disabled={loadingConfigs}
1191
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] transition"
1192
- >
1193
- {loadingConfigs ? "Loading..." : "Refresh"}
1194
- </button>
1195
- <a
1196
- href="https://app.composio.dev/mcp_configs"
1197
- target="_blank"
1198
- rel="noopener noreferrer"
1199
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-accent)] transition"
1200
- >
1201
- Create Config →
1202
- </a>
1203
- </div>
1204
- </div>
1205
-
1206
- {loadingConfigs ? (
1207
- <div className="text-center py-6 text-[var(--color-text-muted)]">Loading configs...</div>
1208
- ) : composioConfigs.length === 0 ? (
1209
- <div className="bg-[var(--color-surface)] card p-4 text-center">
1210
- <p className="text-sm text-[var(--color-text-muted)]">No MCP configs found</p>
1211
- <p className="text-xs text-[var(--color-text-faint)] mt-2">
1212
- First <button onClick={() => setSubTab("connect")} className="text-[var(--color-accent)] hover:text-[var(--color-accent-hover)]">connect some apps</button>, then create a config.
1213
- </p>
1214
- <a
1215
- href="https://app.composio.dev/mcp_configs"
1216
- target="_blank"
1217
- rel="noopener noreferrer"
1218
- className="text-xs text-[var(--color-accent)] hover:text-[var(--color-accent-hover)] mt-2 inline-block"
1219
- >
1220
- Create in Composio →
1221
- </a>
1222
- </div>
1223
- ) : (
1224
- <div className="space-y-2">
1225
- {composioConfigs.map((config) => {
1226
- const added = isConfigAdded(config.id);
1227
- const isAdding = addingConfig === config.id;
1228
- return (
1229
- <div
1230
- key={config.id}
1231
- className={`bg-[var(--color-surface)] border rounded-lg p-3 transition flex items-center justify-between ${
1232
- added ? "border-green-500/30" : "border-[var(--color-border)] hover:border-[var(--color-border-light)]"
1233
- }`}
1234
- >
1235
- <div className="flex-1 min-w-0">
1236
- <div className="flex items-center gap-2">
1237
- <span className="font-medium text-sm">{config.name}</span>
1238
- <span className="text-xs text-[var(--color-text-faint)]">{config.toolsCount} tools</span>
1239
- {added && (
1240
- <span className="text-xs text-green-400">Added</span>
1241
- )}
1242
- </div>
1243
- {config.toolkits.length > 0 && (
1244
- <div className="flex flex-wrap gap-1 mt-1">
1245
- {config.toolkits.slice(0, 4).map((toolkit) => (
1246
- <span
1247
- key={toolkit}
1248
- className="text-xs bg-[var(--color-surface-raised)] text-[var(--color-text-muted)] px-1.5 py-0.5 rounded"
1249
- >
1250
- {toolkit}
1251
- </span>
1252
- ))}
1253
- {config.toolkits.length > 4 && (
1254
- <span className="text-xs text-[var(--color-text-faint)]">+{config.toolkits.length - 4}</span>
1255
- )}
1256
- </div>
1257
- )}
1258
- </div>
1259
- <div className="flex items-center gap-2 ml-3">
1260
- {added ? (
1261
- <span className="text-xs text-[var(--color-text-faint)] px-2 py-1">In Servers</span>
1262
- ) : (
1263
- <button
1264
- onClick={() => addComposioConfig(config.id)}
1265
- disabled={isAdding}
1266
- className="text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black px-3 py-1 rounded font-medium transition disabled:opacity-50"
1267
- >
1268
- {isAdding ? "Adding..." : "Add"}
1269
- </button>
1270
- )}
1271
- <a
1272
- href={`https://app.composio.dev/mcp_configs/${config.id}`}
1273
- target="_blank"
1274
- rel="noopener noreferrer"
1275
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] transition"
1276
- >
1277
- Edit
1278
- </a>
1279
- </div>
1280
- </div>
1281
- );
1282
- })}
1283
- </div>
1284
- )}
1285
- </div>
1286
- )}
1287
- </>
1288
- )}
1289
-
1290
- {/* Smithery Content */}
1291
- {smitheryConnected && (connectedCount === 1 || activeProvider === "smithery") && (
1292
- <div>
1293
- {connectedCount === 1 && (
1294
- <div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)] mb-4">
1295
- <span className="w-2 h-2 rounded-full bg-blue-500" />
1296
- Smithery
1297
- <span className="text-green-400">Connected</span>
1298
- </div>
1299
- )}
1300
- <div className="flex items-center justify-between mb-3">
1301
- <p className="text-sm text-[var(--color-text-muted)]">
1302
- Add MCP servers from the Smithery registry
1303
- </p>
1304
- <a
1305
- href="https://smithery.ai/servers"
1306
- target="_blank"
1307
- rel="noopener noreferrer"
1308
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-accent)] transition"
1309
- >
1310
- Browse Smithery →
1311
- </a>
1312
- </div>
1313
- <div className="bg-[var(--color-surface)] card p-4 text-center">
1314
- <p className="text-sm text-[var(--color-text-muted)]">
1315
- Smithery servers can be added from the <strong>Browse Registry</strong> tab.
1316
- </p>
1317
- <p className="text-xs text-[var(--color-text-faint)] mt-2">
1318
- Your API key will be used automatically when adding Smithery servers.
1319
- </p>
1320
- </div>
1321
- </div>
1322
- )}
1323
-
1324
- {/* AgentDojo Content */}
1325
- {agentDojoConnected && (connectedCount === 1 || activeProvider === "agentdojo") && (
1326
- <AgentDojoContent
1327
- projectId={projectId}
1328
- onServerAdded={onServerAdded}
1329
- showProviderBadge={connectedCount === 1}
1330
- />
1331
- )}
1332
-
1333
- <div className="p-3 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded text-xs text-[var(--color-text-muted)]">
1334
- <strong className="text-[var(--color-text-secondary)]">Tip:</strong> Connect apps first, then add MCP configs to make tools available to your agents.
1335
- {" · "}
1336
- <a href="/settings" className="text-[var(--color-accent)] hover:text-[var(--color-accent-hover)]">Add more providers in Settings</a>
1337
- </div>
1338
- </div>
1339
- </>
1340
- );
1341
- }
1342
-
1343
- // AgentDojo Content Component
1344
- interface AgentDojoConfig {
1345
- id: string;
1346
- name: string;
1347
- slug: string;
1348
- toolkits: string[];
1349
- toolsCount: number;
1350
- mcpUrl: string;
1351
- createdAt?: string;
1352
- }
1353
-
1354
- function AgentDojoContent({
1355
- projectId,
1356
- onServerAdded,
1357
- showProviderBadge,
1358
- }: {
1359
- projectId?: string | null;
1360
- onServerAdded?: () => void;
1361
- showProviderBadge?: boolean;
1362
- }) {
1363
- const { authFetch } = useAuth();
1364
- const [subTab, setSubTab] = useState<"configs" | "toolkits">("configs");
1365
- const [configs, setConfigs] = useState<AgentDojoConfig[]>([]);
1366
- const [addedServers, setAddedServers] = useState<Set<string>>(new Set());
1367
- const [loadingConfigs, setLoadingConfigs] = useState(false);
1368
- const [addingConfig, setAddingConfig] = useState<string | null>(null);
1369
- const { alert, AlertDialog } = useAlert();
1370
-
1371
- const fetchConfigs = async () => {
1372
- setLoadingConfigs(true);
1373
- try {
1374
- const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
1375
- const serversUrl = projectId && projectId !== "unassigned"
1376
- ? `/api/mcp/servers?project=${encodeURIComponent(projectId)}`
1377
- : "/api/mcp/servers";
1378
- console.log(`[AgentDojo:fetchConfigs] projectId=${projectId} serversUrl=${serversUrl}`);
1379
- const [configsRes, serversRes] = await Promise.all([
1380
- authFetch(`/api/integrations/agentdojo/configs${projectParam}`),
1381
- authFetch(serversUrl),
1382
- ]);
1383
- const configsData = await configsRes.json();
1384
- const serversData = await serversRes.json();
1385
-
1386
- console.log(`[AgentDojo:fetchConfigs] configs=${(configsData.configs || []).length} servers=${(serversData.servers || []).length}`);
1387
- setConfigs(configsData.configs || []);
1388
-
1389
- // Track which configs are already added as local servers
1390
- const agentdojoServers = (serversData.servers || []).filter((s: any) => s.source === "agentdojo");
1391
- console.log(`[AgentDojo:fetchConfigs] agentdojo servers found: ${agentdojoServers.length}`);
1392
- for (const s of agentdojoServers) {
1393
- const match = s.url?.match(/\/mcp\/([^/?]+)/);
1394
- console.log(`[AgentDojo:fetchConfigs] server: id=${s.id} name=${s.name} project_id=${s.project_id} url=${s.url?.substring(0, 80)} extracted=${match ? match[1] : s.name}`);
1395
- }
1396
- const agentdojoServerIds = new Set(
1397
- agentdojoServers.map((s: any) => {
1398
- // Extract config ID from URL or match by name
1399
- const match = s.url?.match(/\/mcp\/([^/?]+)/);
1400
- return match ? match[1] : s.name;
1401
- })
1402
- );
1403
- console.log(`[AgentDojo:fetchConfigs] addedServers set:`, [...agentdojoServerIds]);
1404
- setAddedServers(agentdojoServerIds);
1405
- } catch (e) {
1406
- console.error("Failed to fetch AgentDojo configs:", e);
1407
- }
1408
- setLoadingConfigs(false);
1409
- };
1410
-
1411
- const addConfig = async (configId: string) => {
1412
- setAddingConfig(configId);
1413
- try {
1414
- const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
1415
- console.log(`[AgentDojo:addConfig] configId=${configId} projectParam=${projectParam}`);
1416
- const res = await authFetch(`/api/integrations/agentdojo/configs/${configId}/add${projectParam}`, {
1417
- method: "POST",
1418
- });
1419
- const data = await res.json();
1420
- console.log(`[AgentDojo:addConfig] response status=${res.status} ok=${res.ok} message=${data.message} server.id=${data.server?.id} server.project_id=${data.server?.project_id}`);
1421
- if (res.ok) {
1422
- const config = configs.find(c => c.id === configId);
1423
- const addKey = config?.slug || configId;
1424
- console.log(`[AgentDojo:addConfig] marking as added: key=${addKey} config.slug=${config?.slug} config.id=${config?.id} config.name=${config?.name}`);
1425
- setAddedServers(prev => new Set([...prev, addKey]));
1426
- onServerAdded?.();
1427
- } else {
1428
- await alert(data.error || "Failed to add config", { title: "Error", variant: "error" });
1429
- }
1430
- } catch (e) {
1431
- console.error("Failed to add config:", e);
1432
- }
1433
- setAddingConfig(null);
1434
- };
1435
-
1436
- const isConfigAdded = (config: AgentDojoConfig) => {
1437
- return addedServers.has(config.slug) || addedServers.has(config.id) || addedServers.has(config.name);
1438
- };
1439
-
1440
- useEffect(() => {
1441
- fetchConfigs();
1442
- }, [authFetch, projectId]);
1443
-
1444
- return (
1445
- <>
1446
- {AlertDialog}
1447
- <div>
1448
- {showProviderBadge && (
1449
- <div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)] mb-4">
1450
- <span className="w-2 h-2 rounded-full bg-green-500" />
1451
- AgentDojo
1452
- <span className="text-green-400">Connected</span>
1453
- </div>
1454
- )}
1455
-
1456
- {/* Sub-tabs */}
1457
- <div className="flex items-center justify-between mb-4">
1458
- <div className="flex gap-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded-lg p-1">
1459
- <button
1460
- onClick={() => setSubTab("configs")}
1461
- className={`px-4 py-2 rounded text-sm font-medium transition ${
1462
- subTab === "configs"
1463
- ? "bg-[var(--color-surface-raised)] text-white"
1464
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1465
- }`}
1466
- >
1467
- MCP Servers
1468
- </button>
1469
- <button
1470
- onClick={() => setSubTab("toolkits")}
1471
- className={`px-4 py-2 rounded text-sm font-medium transition ${
1472
- subTab === "toolkits"
1473
- ? "bg-[var(--color-surface-raised)] text-white"
1474
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1475
- }`}
1476
- >
1477
- Browse Toolkits
1478
- </button>
1479
- </div>
1480
- </div>
1481
-
1482
- {/* MCP Servers Tab */}
1483
- {subTab === "configs" && (
1484
- <div>
1485
- <div className="flex items-center justify-between mb-3">
1486
- <p className="text-sm text-[var(--color-text-muted)]">
1487
- Your MCP servers from AgentDojo
1488
- </p>
1489
- <button
1490
- onClick={fetchConfigs}
1491
- disabled={loadingConfigs}
1492
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] transition"
1493
- >
1494
- {loadingConfigs ? "Loading..." : "Refresh"}
1495
- </button>
1496
- </div>
1497
-
1498
- {loadingConfigs ? (
1499
- <div className="text-center py-6 text-[var(--color-text-muted)]">Loading servers...</div>
1500
- ) : configs.length === 0 ? (
1501
- <div className="bg-[var(--color-surface)] card p-4 text-center">
1502
- <p className="text-sm text-[var(--color-text-muted)]">No MCP servers found</p>
1503
- <p className="text-xs text-[var(--color-text-faint)] mt-2">
1504
- <button onClick={() => setSubTab("toolkits")} className="text-[var(--color-accent)] hover:text-[var(--color-accent-hover)]">
1505
- Browse toolkits
1506
- </button>
1507
- {" "}to create a new MCP server.
1508
- </p>
1509
- </div>
1510
- ) : (
1511
- <div className="space-y-2">
1512
- {configs.map((config) => {
1513
- const added = isConfigAdded(config);
1514
- const isAdding = addingConfig === config.id;
1515
- return (
1516
- <div
1517
- key={config.id}
1518
- className={`bg-[var(--color-surface)] border rounded-lg p-3 transition flex items-center justify-between ${
1519
- added ? "border-green-500/30" : "border-[var(--color-border)] hover:border-[var(--color-border-light)]"
1520
- }`}
1521
- >
1522
- <div className="flex-1 min-w-0">
1523
- <div className="flex items-center gap-2">
1524
- <span className="font-medium text-sm">{config.name}</span>
1525
- <span className="text-xs text-[var(--color-text-faint)]">{config.toolsCount} tools</span>
1526
- {added && (
1527
- <span className="text-xs text-green-400">Added</span>
1528
- )}
1529
- </div>
1530
- {config.mcpUrl && (
1531
- <code className="text-xs text-[var(--color-text-faint)] mt-1 block truncate">
1532
- {config.mcpUrl}
1533
- </code>
1534
- )}
1535
- {!config.mcpUrl && config.slug && (
1536
- <code className="text-xs text-[var(--color-text-faint)] mt-1 block truncate">
1537
- {config.slug}
1538
- </code>
1539
- )}
1540
- </div>
1541
- <div className="flex items-center gap-2 ml-3">
1542
- {added ? (
1543
- <span className="text-xs text-[var(--color-text-faint)] px-2 py-1">In Servers</span>
1544
- ) : (
1545
- <button
1546
- onClick={() => addConfig(config.id)}
1547
- disabled={isAdding}
1548
- className="text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black px-3 py-1 rounded font-medium transition disabled:opacity-50"
1549
- >
1550
- {isAdding ? "Adding..." : "Add"}
1551
- </button>
1552
- )}
1553
- </div>
1554
- </div>
1555
- );
1556
- })}
1557
- </div>
1558
- )}
1559
- </div>
1560
- )}
1561
-
1562
- {/* Browse Toolkits Tab */}
1563
- {subTab === "toolkits" && (
1564
- <div>
1565
- <p className="text-sm text-[var(--color-text-muted)] mb-4">
1566
- Browse available toolkits and create MCP servers
1567
- </p>
1568
- <IntegrationsPanel
1569
- providerId="agentdojo"
1570
- projectId={projectId}
1571
- onConnectionComplete={() => {
1572
- fetchConfigs();
1573
- }}
1574
- />
1575
- </div>
1576
- )}
1577
- </div>
1578
- </>
1579
- );
1580
- }
1581
-
1582
- // Parse command and extract credential placeholders
1583
- function parseCommandForCredentials(cmd: string): {
1584
- cleanCommand: string;
1585
- credentials: Array<{ key: string; flag: string }>;
1586
- serverName: string | null;
1587
- } {
1588
- const credentials: Array<{ key: string; flag: string }> = [];
1589
- let cleanCommand = cmd;
1590
- let serverName: string | null = null;
1591
-
1592
- // Try to extract server name from package (e.g., pushover-mcp@latest -> pushover)
1593
- const pkgMatch = cmd.match(/(?:npx\s+-y\s+)?(@?[\w-]+\/)?(@?[\w-]+)(?:@[\w.-]+)?/);
1594
- if (pkgMatch) {
1595
- const pkg = pkgMatch[2] || pkgMatch[1];
1596
- if (pkg) {
1597
- // Extract name: "pushover-mcp" -> "pushover", "@org/server-github" -> "github"
1598
- serverName = pkg
1599
- .replace(/^@/, '')
1600
- .replace(/-mcp$/, '')
1601
- .replace(/-server$/, '')
1602
- .replace(/^server-/, '')
1603
- .replace(/^mcp-/, '');
1604
- }
1605
- }
1606
-
1607
- // Pattern: --flag YOUR_VALUE, --flag <value>, --flag {value}, --flag $VALUE
1608
- // Matches: --token YOUR_TOKEN, --user YOUR_USER, --api-key <API_KEY>, etc.
1609
- const argPattern = /--(\w+[-\w]*)\s+(YOUR_\w+|<[\w_]+>|\{[\w_]+\}|\$[\w_]+|[\w_]*(?:TOKEN|KEY|SECRET|PASSWORD|USER|ID|APIKEY)[\w_]*)/gi;
1610
-
1611
- let match;
1612
- while ((match = argPattern.exec(cmd)) !== null) {
1613
- const flag = match[1];
1614
- const placeholder = match[2];
1615
-
1616
- // Convert flag to env var name: api-key -> API_KEY, token -> TOKEN
1617
- const envKey = flag.toUpperCase().replace(/-/g, '_');
1618
-
1619
- // Add prefix based on server name if available
1620
- const fullKey = serverName
1621
- ? `${serverName.toUpperCase().replace(/-/g, '_')}_${envKey}`
1622
- : envKey;
1623
-
1624
- credentials.push({ key: fullKey, flag });
1625
-
1626
- // Replace placeholder with $ENV_VAR reference in command
1627
- cleanCommand = cleanCommand.replace(
1628
- new RegExp(`(--${flag}\\s+)${placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i'),
1629
- `--${flag} $${fullKey}`
1630
- );
1631
- }
1632
-
1633
- return { cleanCommand, credentials, serverName };
1634
- }
1635
-
1636
- function AddServerModal({
1637
- onClose,
1638
- onAdded,
1639
- projects,
1640
- defaultProjectId,
1641
- }: {
1642
- onClose: () => void;
1643
- onAdded: () => void;
1644
- projects?: Array<{ id: string; name: string; color: string }>;
1645
- defaultProjectId?: string | null;
1646
- }) {
1647
- const { authFetch } = useAuth();
1648
- const [mode, setMode] = useState<"npm" | "pip" | "command" | "http">("npm");
1649
- const [name, setName] = useState("");
1650
- const [pkg, setPkg] = useState("");
1651
- const [pipModule, setPipModule] = useState("");
1652
- const [command, setCommand] = useState("");
1653
- const [url, setUrl] = useState("");
1654
- const [username, setUsername] = useState("");
1655
- const [password, setPassword] = useState("");
1656
- const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
1657
- const [projectId, setProjectId] = useState<string | null>(defaultProjectId || null);
1658
- const [saving, setSaving] = useState(false);
1659
- const [error, setError] = useState<string | null>(null);
1660
-
1661
- const hasProjects = projects && projects.length > 0;
1662
-
1663
- const addEnvVar = () => {
1664
- setEnvVars([...envVars, { key: "", value: "" }]);
1665
- };
1666
-
1667
- const updateEnvVar = (index: number, field: "key" | "value", value: string) => {
1668
- const updated = [...envVars];
1669
- updated[index][field] = value;
1670
- setEnvVars(updated);
1671
- };
1672
-
1673
- const removeEnvVar = (index: number) => {
1674
- setEnvVars(envVars.filter((_, i) => i !== index));
1675
- };
1676
-
1677
- // Handle command input - parse and extract credentials
1678
- const handleCommandChange = (value: string) => {
1679
- setCommand(value);
1680
-
1681
- // Only parse if it looks like a full command with placeholders
1682
- if (value.includes('YOUR_') || value.includes('<') || value.includes('{') ||
1683
- /TOKEN|KEY|SECRET|PASSWORD/i.test(value)) {
1684
- const { cleanCommand, credentials, serverName } = parseCommandForCredentials(value);
1685
-
1686
- // Auto-set name if empty
1687
- if (!name && serverName) {
1688
- setName(serverName);
1689
- }
1690
-
1691
- // Add any new credentials that don't already exist
1692
- if (credentials.length > 0) {
1693
- const existingKeys = new Set(envVars.map(e => e.key));
1694
- const newVars = credentials
1695
- .filter(c => !existingKeys.has(c.key))
1696
- .map(c => ({ key: c.key, value: "" }));
1697
-
1698
- if (newVars.length > 0) {
1699
- setEnvVars([...envVars, ...newVars]);
1700
- // Update command to use clean version with env var references
1701
- setCommand(cleanCommand);
1702
- }
1703
- }
1704
- }
1705
- };
1706
-
1707
- // Handle package input - detect if user pasted a full command
1708
- const handlePackageChange = (value: string) => {
1709
- // Check if this looks like a full command (has npx, spaces with args, or credential placeholders)
1710
- const looksLikeCommand =
1711
- value.startsWith('npx ') ||
1712
- value.includes(' --') ||
1713
- value.includes('YOUR_') ||
1714
- value.includes('<') ||
1715
- /\s+(TOKEN|KEY|SECRET|PASSWORD)/i.test(value);
1716
-
1717
- if (looksLikeCommand) {
1718
- // Switch to command mode and parse
1719
- setMode("command");
1720
- handleCommandChange(value);
1721
- } else {
1722
- // Just a package name
1723
- setPkg(value);
1724
-
1725
- // Try to auto-set name from package
1726
- if (!name && value) {
1727
- const serverName = value
1728
- .replace(/^@[\w-]+\//, '') // Remove org prefix
1729
- .replace(/@[\w.-]+$/, '') // Remove version
1730
- .replace(/^server-/, '')
1731
- .replace(/-server$/, '')
1732
- .replace(/^mcp-/, '')
1733
- .replace(/-mcp$/, '');
1734
- if (serverName && serverName !== value) {
1735
- setName(serverName);
1736
- }
1737
- }
1738
- }
1739
- };
1740
-
1741
- const handleAdd = async () => {
1742
- if (!name) {
1743
- setError("Name is required");
1744
- return;
1745
- }
1746
-
1747
- if (mode === "npm" && !pkg) {
1748
- setError("npm package is required");
1749
- return;
1750
- }
1751
-
1752
- if (mode === "pip" && !pkg) {
1753
- setError("pip package is required");
1754
- return;
1755
- }
1756
-
1757
- if (mode === "command" && !command) {
1758
- setError("Command is required");
1759
- return;
1760
- }
1761
-
1762
- if (mode === "http" && !url) {
1763
- setError("URL is required");
1764
- return;
1765
- }
1766
-
1767
- setSaving(true);
1768
- setError(null);
1769
-
1770
- // Build env object from envVars array
1771
- const env: Record<string, string> = {};
1772
- for (const { key, value } of envVars) {
1773
- if (key.trim()) {
1774
- env[key.trim()] = value;
1775
- }
1776
- }
1777
-
1778
- try {
1779
- const body: Record<string, unknown> = { name };
1780
-
1781
- if (mode === "npm") {
1782
- body.type = "npm";
1783
- body.package = pkg;
1784
- } else if (mode === "pip") {
1785
- body.type = "pip";
1786
- body.package = pkg;
1787
- if (pipModule) {
1788
- body.pip_module = pipModule;
1789
- }
1790
- } else if (mode === "http") {
1791
- body.type = "http";
1792
- body.url = url;
1793
- // Build headers with Basic Auth if credentials provided
1794
- const headers: Record<string, string> = {
1795
- "Content-Type": "application/json",
1796
- };
1797
- if (username && password) {
1798
- // Base64 encode username:password for Basic Auth
1799
- const credentials = btoa(`${username}:${password}`);
1800
- headers["Authorization"] = `Basic ${credentials}`;
1801
- }
1802
- body.headers = headers;
1803
- } else {
1804
- // Parse command into parts
1805
- const parts = command.trim().split(/\s+/);
1806
- body.type = "custom";
1807
- body.command = parts[0];
1808
- body.args = parts.slice(1).join(" ");
1809
- }
1810
-
1811
- if (Object.keys(env).length > 0) {
1812
- body.env = env;
1813
- }
1814
-
1815
- // Add project_id if selected
1816
- if (projectId) {
1817
- body.project_id = projectId;
1818
- }
1819
-
1820
- const res = await authFetch("/api/mcp/servers", {
1821
- method: "POST",
1822
- headers: { "Content-Type": "application/json" },
1823
- body: JSON.stringify(body),
1824
- });
1825
-
1826
- if (!res.ok) {
1827
- const data = await res.json();
1828
- setError(data.error || "Failed to add server");
1829
- setSaving(false);
1830
- return;
1831
- }
1832
-
1833
- onAdded();
1834
- } catch (e) {
1835
- setError("Failed to add server");
1836
- setSaving(false);
1837
- }
1838
- };
1839
-
1840
- const quickAdd = (serverName: string, serverPkg: string) => {
1841
- setMode("npm");
1842
- setName(serverName);
1843
- setPkg(serverPkg);
1844
- };
1845
-
1846
- return (
1847
- <div className="fixed inset-0 bg-black/50 backdrop-blur-[2px] z-50 flex items-center justify-center p-4">
1848
- <div className="bg-[var(--color-surface)] card w-full max-w-lg max-h-[90vh] overflow-y-auto">
1849
- <div className="p-4 border-b border-[var(--color-border)] flex items-center justify-between sticky top-0 bg-[var(--color-surface)]">
1850
- <h2 className="text-lg font-semibold">Add MCP Server</h2>
1851
- <button onClick={onClose} className="text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]">
1852
-
1853
- </button>
1854
- </div>
1855
-
1856
- <div className="p-4 space-y-4">
1857
- {/* Quick picks */}
1858
- <div>
1859
- <p className="text-sm text-[var(--color-text-muted)] mb-2">Quick add:</p>
1860
- <div className="flex flex-wrap gap-2">
1861
- {[
1862
- { name: "filesystem", pkg: "@modelcontextprotocol/server-filesystem", type: "npm" as const },
1863
- { name: "fetch", pkg: "@modelcontextprotocol/server-fetch", type: "npm" as const },
1864
- { name: "memory", pkg: "@modelcontextprotocol/server-memory", type: "npm" as const },
1865
- { name: "github", pkg: "@modelcontextprotocol/server-github", type: "npm" as const },
1866
- { name: "time", pkg: "mcp-server-time", module: "mcp_server_time", type: "pip" as const },
1867
- ].map(s => (
1868
- <button
1869
- key={s.name}
1870
- onClick={() => {
1871
- setMode(s.type);
1872
- setName(s.name);
1873
- setPkg(s.pkg);
1874
- if (s.type === "pip" && "module" in s) {
1875
- setPipModule(s.module || "");
1876
- } else {
1877
- setPipModule("");
1878
- }
1879
- }}
1880
- className="text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] px-3 py-1 rounded transition"
1881
- >
1882
- {s.name}
1883
- </button>
1884
- ))}
1885
- </div>
1886
- </div>
1887
-
1888
- {/* Mode toggle */}
1889
- <div className="flex gap-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded p-1">
1890
- <button
1891
- onClick={() => setMode("npm")}
1892
- className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
1893
- mode === "npm"
1894
- ? "bg-[var(--color-surface-raised)] text-white"
1895
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1896
- }`}
1897
- >
1898
- npm
1899
- </button>
1900
- <button
1901
- onClick={() => setMode("pip")}
1902
- className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
1903
- mode === "pip"
1904
- ? "bg-[var(--color-surface-raised)] text-white"
1905
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1906
- }`}
1907
- >
1908
- pip
1909
- </button>
1910
- <button
1911
- onClick={() => setMode("command")}
1912
- className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
1913
- mode === "command"
1914
- ? "bg-[var(--color-surface-raised)] text-white"
1915
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1916
- }`}
1917
- >
1918
- Command
1919
- </button>
1920
- <button
1921
- onClick={() => setMode("http")}
1922
- className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
1923
- mode === "http"
1924
- ? "bg-[var(--color-surface-raised)] text-white"
1925
- : "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1926
- }`}
1927
- >
1928
- HTTP
1929
- </button>
1930
- </div>
1931
-
1932
- {/* Name */}
1933
- <div>
1934
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Name</label>
1935
- <input
1936
- type="text"
1937
- value={name}
1938
- onChange={e => setName(e.target.value)}
1939
- placeholder="e.g., pushover"
1940
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)]"
1941
- />
1942
- </div>
1943
-
1944
- {/* Project Scope - only show when projects exist */}
1945
- {hasProjects && (
1946
- <div>
1947
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Scope</label>
1948
- <Select
1949
- value={projectId || ""}
1950
- onChange={(value) => setProjectId(value || null)}
1951
- options={[
1952
- { value: "", label: "Global (all projects)" },
1953
- ...projects!.map(p => ({ value: p.id, label: p.name }))
1954
- ]}
1955
- placeholder="Select scope..."
1956
- />
1957
- <p className="text-xs text-[var(--color-text-faint)] mt-1">
1958
- Global servers are available to all agents. Project-scoped servers are only available to agents in that project.
1959
- </p>
1960
- </div>
1961
- )}
1962
-
1963
- {/* npm Package */}
1964
- {mode === "npm" && (
1965
- <div>
1966
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">npm Package</label>
1967
- <input
1968
- type="text"
1969
- value={pkg}
1970
- onChange={e => handlePackageChange(e.target.value)}
1971
- placeholder="e.g., @modelcontextprotocol/server-filesystem or paste full command"
1972
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)]"
1973
- />
1974
- <p className="text-xs text-[var(--color-text-faint)] mt-1">
1975
- Package name or paste a full npx command with credentials
1976
- </p>
1977
- </div>
1978
- )}
1979
-
1980
- {/* pip Package (Python) */}
1981
- {mode === "pip" && (
1982
- <div className="space-y-4">
1983
- <div>
1984
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">pip Package</label>
1985
- <input
1986
- type="text"
1987
- value={pkg}
1988
- onChange={e => {
1989
- setPkg(e.target.value);
1990
- // Auto-set module from package name
1991
- if (!pipModule && e.target.value) {
1992
- const basePkg = e.target.value.split("[")[0].replace(/-/g, ".");
1993
- setPipModule(basePkg);
1994
- }
1995
- }}
1996
- placeholder="e.g., late-sdk[mcp]"
1997
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)]"
1998
- />
1999
- <p className="text-xs text-[var(--color-text-faint)] mt-1">
2000
- Python package with extras, e.g., late-sdk[mcp] or mcp-server-time
2001
- </p>
2002
- </div>
2003
- <div>
2004
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Module (optional)</label>
2005
- <input
2006
- type="text"
2007
- value={pipModule}
2008
- onChange={e => setPipModule(e.target.value)}
2009
- placeholder="e.g., late.mcp"
2010
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2011
- />
2012
- <p className="text-xs text-[var(--color-text-faint)] mt-1">
2013
- Python module to run with -m. Auto-detected from package name if not specified.
2014
- </p>
2015
- </div>
2016
- </div>
2017
- )}
2018
-
2019
- {/* Custom Command */}
2020
- {mode === "command" && (
2021
- <div>
2022
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Command</label>
2023
- <input
2024
- type="text"
2025
- value={command}
2026
- onChange={e => handleCommandChange(e.target.value)}
2027
- placeholder="e.g., npx -y pushover-mcp@latest start --token YOUR_TOKEN"
2028
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2029
- />
2030
- <p className="text-xs text-[var(--color-text-faint)] mt-1">
2031
- Paste the full command - credentials like YOUR_TOKEN will be auto-extracted
2032
- </p>
2033
- </div>
2034
- )}
2035
-
2036
- {/* HTTP Endpoint */}
2037
- {mode === "http" && (
2038
- <div className="space-y-4">
2039
- <div>
2040
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">URL</label>
2041
- <input
2042
- type="text"
2043
- value={url}
2044
- onChange={e => setUrl(e.target.value)}
2045
- placeholder="e.g., https://example.com/wp-json/mcp/v1/messages"
2046
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2047
- />
2048
- </div>
2049
- <div className="p-3 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded">
2050
- <p className="text-xs text-[var(--color-text-muted)] mb-3">
2051
- Optional: Basic Auth credentials (will be encoded and stored securely)
2052
- </p>
2053
- <div className="grid grid-cols-2 gap-3">
2054
- <div>
2055
- <label className="block text-xs text-[var(--color-text-faint)] mb-1">Username</label>
2056
- <input
2057
- type="text"
2058
- value={username}
2059
- onChange={e => setUsername(e.target.value)}
2060
- placeholder="username"
2061
- className="w-full bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--color-accent)]"
2062
- />
2063
- </div>
2064
- <div>
2065
- <label className="block text-xs text-[var(--color-text-faint)] mb-1">Password</label>
2066
- <input
2067
- type="password"
2068
- value={password}
2069
- onChange={e => setPassword(e.target.value)}
2070
- placeholder="password or app key"
2071
- className="w-full bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--color-accent)]"
2072
- />
2073
- </div>
2074
- </div>
2075
- </div>
2076
- </div>
2077
- )}
2078
-
2079
- {/* Environment Variables / Credentials */}
2080
- <div>
2081
- <div className="flex items-center justify-between mb-2">
2082
- <label className="text-sm text-[var(--color-text-muted)]">
2083
- Environment Variables / Credentials
2084
- </label>
2085
- <button
2086
- onClick={addEnvVar}
2087
- className="text-xs text-[var(--color-accent)] hover:text-[var(--color-accent-hover)] transition"
2088
- >
2089
- + Add Variable
2090
- </button>
2091
- </div>
2092
-
2093
- {envVars.length === 0 && (
2094
- <p className="text-xs text-[var(--color-text-faint)] bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded p-3">
2095
- Add environment variables for API tokens and credentials.
2096
- These are stored encrypted and passed to the server at startup.
2097
- </p>
2098
- )}
2099
-
2100
- {envVars.length > 0 && (
2101
- <div className="space-y-2">
2102
- {envVars.map((env, index) => (
2103
- <div key={index} className="flex gap-2">
2104
- <input
2105
- type="text"
2106
- value={env.key}
2107
- onChange={e => updateEnvVar(index, "key", e.target.value)}
2108
- placeholder="KEY"
2109
- className="w-1/3 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-2 py-1.5 text-sm font-mono focus:outline-none focus:border-[var(--color-accent)]"
2110
- />
2111
- <input
2112
- type="password"
2113
- value={env.value}
2114
- onChange={e => updateEnvVar(index, "value", e.target.value)}
2115
- placeholder="value"
2116
- className="flex-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-2 py-1.5 text-sm font-mono focus:outline-none focus:border-[var(--color-accent)]"
2117
- />
2118
- <button
2119
- onClick={() => removeEnvVar(index)}
2120
- className="text-[var(--color-text-muted)] hover:text-red-400 px-2 transition"
2121
- >
2122
-
2123
- </button>
2124
- </div>
2125
- ))}
2126
- </div>
2127
- )}
2128
- </div>
2129
-
2130
- {error && <p className="text-red-400 text-sm">{error}</p>}
2131
- </div>
2132
-
2133
- <div className="p-4 border-t border-[var(--color-border)] flex justify-end gap-2 sticky bottom-0 bg-[var(--color-surface)]">
2134
- <button
2135
- onClick={onClose}
2136
- className="px-4 py-2 border border-[var(--color-border-light)] hover:border-[var(--color-text-muted)] rounded transition"
2137
- >
2138
- Cancel
2139
- </button>
2140
- <button
2141
- onClick={handleAdd}
2142
- disabled={saving || !name || (mode === "npm" ? !pkg : mode === "pip" ? !pkg : mode === "http" ? !url : !command)}
2143
- className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black rounded font-medium transition disabled:opacity-50"
2144
- >
2145
- {saving ? "Adding..." : "Add Server"}
2146
- </button>
2147
- </div>
2148
- </div>
2149
- </div>
2150
- );
2151
- }
2152
-
2153
- function EditServerModal({
2154
- server,
2155
- projects,
2156
- onClose,
2157
- onSaved,
2158
- }: {
2159
- server: McpServer;
2160
- projects?: Array<{ id: string; name: string; color: string }>;
2161
- onClose: () => void;
2162
- onSaved: () => void;
2163
- }) {
2164
- const { authFetch } = useAuth();
2165
- const [name, setName] = useState(server.name);
2166
- const [pkg, setPkg] = useState(server.package || "");
2167
- const [command, setCommand] = useState(server.command || "");
2168
- const [args, setArgs] = useState(server.args || "");
2169
- const [url, setUrl] = useState(server.url || "");
2170
- // Extract username/password from existing Basic Auth header
2171
- const [username, setUsername] = useState(() => {
2172
- const authHeader = server.headers?.["Authorization"] || "";
2173
- if (authHeader.startsWith("Basic ")) {
2174
- try {
2175
- const decoded = atob(authHeader.slice(6));
2176
- return decoded.split(":")[0] || "";
2177
- } catch { return ""; }
2178
- }
2179
- return "";
2180
- });
2181
- const [password, setPassword] = useState(() => {
2182
- const authHeader = server.headers?.["Authorization"] || "";
2183
- if (authHeader.startsWith("Basic ")) {
2184
- try {
2185
- const decoded = atob(authHeader.slice(6));
2186
- const parts = decoded.split(":");
2187
- return parts.slice(1).join(":") || "";
2188
- } catch { return ""; }
2189
- }
2190
- return "";
2191
- });
2192
- const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>(() => {
2193
- // Convert env object to array format
2194
- return Object.entries(server.env || {}).map(([key, value]) => ({ key, value }));
2195
- });
2196
- const [projectId, setProjectId] = useState<string | null>(server.project_id);
2197
- const [saving, setSaving] = useState(false);
2198
- const [error, setError] = useState<string | null>(null);
2199
-
2200
- const hasProjects = projects && projects.length > 0;
2201
- const isRemote = server.type === "http";
2202
-
2203
- const addEnvVar = () => {
2204
- setEnvVars([...envVars, { key: "", value: "" }]);
2205
- };
2206
-
2207
- const updateEnvVar = (index: number, field: "key" | "value", value: string) => {
2208
- const updated = [...envVars];
2209
- updated[index][field] = value;
2210
- setEnvVars(updated);
2211
- };
2212
-
2213
- const removeEnvVar = (index: number) => {
2214
- setEnvVars(envVars.filter((_, i) => i !== index));
2215
- };
2216
-
2217
- const handleSave = async () => {
2218
- if (!name.trim()) {
2219
- setError("Name is required");
2220
- return;
2221
- }
2222
-
2223
- setSaving(true);
2224
- setError(null);
2225
-
2226
- // Build env object from envVars array
2227
- const env: Record<string, string> = {};
2228
- for (const { key, value } of envVars) {
2229
- if (key.trim()) {
2230
- env[key.trim()] = value;
2231
- }
2232
- }
2233
-
2234
- try {
2235
- const updates: Record<string, unknown> = {
2236
- name: name.trim(),
2237
- env,
2238
- };
2239
-
2240
- // Only include fields that are relevant to the server type
2241
- if (isRemote) {
2242
- // HTTP server - update URL and headers
2243
- if (url.trim()) {
2244
- updates.url = url.trim();
2245
- }
2246
- // Build headers with Basic Auth if credentials provided
2247
- const headers: Record<string, string> = {
2248
- "Content-Type": "application/json",
2249
- };
2250
- if (username && password) {
2251
- const credentials = btoa(`${username}:${password}`);
2252
- headers["Authorization"] = `Basic ${credentials}`;
2253
- }
2254
- updates.headers = headers;
2255
- } else {
2256
- if (server.type === "npm" && pkg.trim()) {
2257
- updates.package = pkg.trim();
2258
- }
2259
- if (server.type === "pip" && pkg.trim()) {
2260
- updates.package = pkg.trim();
2261
- }
2262
- if (server.type === "custom") {
2263
- if (command.trim()) updates.command = command.trim();
2264
- if (args.trim()) updates.args = args.trim();
2265
- }
2266
- }
2267
-
2268
- // Include project_id update
2269
- updates.project_id = projectId;
2270
-
2271
- const res = await authFetch(`/api/mcp/servers/${server.id}`, {
2272
- method: "PUT",
2273
- headers: { "Content-Type": "application/json" },
2274
- body: JSON.stringify(updates),
2275
- });
2276
-
2277
- if (!res.ok) {
2278
- const data = await res.json();
2279
- setError(data.error || "Failed to save changes");
2280
- setSaving(false);
2281
- return;
2282
- }
2283
-
2284
- // If server was running, restart it to apply new env vars
2285
- if (server.status === "running" && !isRemote) {
2286
- try {
2287
- // Stop the server
2288
- await authFetch(`/api/mcp/servers/${server.id}/stop`, { method: "POST" });
2289
- // Start it again
2290
- await authFetch(`/api/mcp/servers/${server.id}/start`, { method: "POST" });
2291
- } catch (e) {
2292
- console.error("Failed to restart server:", e);
2293
- // Don't fail the save, just log the error
2294
- }
2295
- }
2296
-
2297
- onSaved();
2298
- } catch (e) {
2299
- setError("Failed to save changes");
2300
- setSaving(false);
2301
- }
2302
- };
2303
-
2304
- return (
2305
- <div className="fixed inset-0 bg-black/50 backdrop-blur-[2px] z-50 flex items-center justify-center p-4">
2306
- <div className="bg-[var(--color-surface)] card w-full max-w-lg max-h-[90vh] overflow-y-auto">
2307
- <div className="p-4 border-b border-[var(--color-border)] flex items-center justify-between sticky top-0 bg-[var(--color-surface)]">
2308
- <h2 className="text-lg font-semibold">Edit MCP Server</h2>
2309
- <button onClick={onClose} className="text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]">
2310
-
2311
- </button>
2312
- </div>
2313
-
2314
- <div className="p-4 space-y-4">
2315
- {/* Server Type Info */}
2316
- <div className="text-sm text-[var(--color-text-muted)] bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded p-3">
2317
- Type: <span className="text-[var(--color-text-secondary)]">{server.type}</span>
2318
- {server.package && <> • Package: <span className="text-[var(--color-text-secondary)] font-mono">{server.package}</span></>}
2319
- {server.command && <> • Command: <span className="text-[var(--color-text-secondary)] font-mono">{server.command}</span></>}
2320
- </div>
2321
-
2322
- {/* Name */}
2323
- <div>
2324
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Name</label>
2325
- <input
2326
- type="text"
2327
- value={name}
2328
- onChange={e => setName(e.target.value)}
2329
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)]"
2330
- />
2331
- </div>
2332
-
2333
- {/* Project Scope */}
2334
- {hasProjects && (
2335
- <div>
2336
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Scope</label>
2337
- <Select
2338
- value={projectId || ""}
2339
- onChange={(value) => setProjectId(value || null)}
2340
- options={[
2341
- { value: "", label: "Global (all projects)" },
2342
- ...projects!.map(p => ({ value: p.id, label: p.name }))
2343
- ]}
2344
- placeholder="Select scope..."
2345
- />
2346
- </div>
2347
- )}
2348
-
2349
- {/* Package (for npm type) */}
2350
- {server.type === "npm" && (
2351
- <div>
2352
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">npm Package</label>
2353
- <input
2354
- type="text"
2355
- value={pkg}
2356
- onChange={e => setPkg(e.target.value)}
2357
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2358
- />
2359
- </div>
2360
- )}
2361
-
2362
- {/* Package (for pip type) */}
2363
- {server.type === "pip" && (
2364
- <div>
2365
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">pip Package</label>
2366
- <input
2367
- type="text"
2368
- value={pkg}
2369
- onChange={e => setPkg(e.target.value)}
2370
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2371
- />
2372
- </div>
2373
- )}
2374
-
2375
- {/* URL & Credentials (for http type) */}
2376
- {isRemote && (
2377
- <>
2378
- <div>
2379
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Server URL</label>
2380
- <input
2381
- type="text"
2382
- value={url}
2383
- onChange={e => setUrl(e.target.value)}
2384
- placeholder="https://example.com/mcp"
2385
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2386
- />
2387
- </div>
2388
- <div>
2389
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Authentication (Basic Auth)</label>
2390
- <div className="flex gap-2">
2391
- <input
2392
- type="text"
2393
- value={username}
2394
- onChange={e => setUsername(e.target.value)}
2395
- placeholder="Username"
2396
- className="flex-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--color-accent)]"
2397
- />
2398
- <input
2399
- type="password"
2400
- value={password}
2401
- onChange={e => setPassword(e.target.value)}
2402
- placeholder="Password / App Password"
2403
- className="flex-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--color-accent)]"
2404
- />
2405
- </div>
2406
- <p className="text-xs text-[var(--color-text-faint)] mt-1">
2407
- Leave empty if no authentication required
2408
- </p>
2409
- </div>
2410
- </>
2411
- )}
2412
-
2413
- {/* Command & Args (for custom type) */}
2414
- {server.type === "custom" && (
2415
- <>
2416
- <div>
2417
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Command</label>
2418
- <input
2419
- type="text"
2420
- value={command}
2421
- onChange={e => setCommand(e.target.value)}
2422
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2423
- />
2424
- </div>
2425
- <div>
2426
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Arguments</label>
2427
- <input
2428
- type="text"
2429
- value={args}
2430
- onChange={e => setArgs(e.target.value)}
2431
- placeholder="e.g., --token $TOKEN --verbose"
2432
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[var(--color-accent)]"
2433
- />
2434
- </div>
2435
- </>
2436
- )}
2437
-
2438
- {/* Environment Variables */}
2439
- {!isRemote && (
2440
- <div>
2441
- <div className="flex items-center justify-between mb-2">
2442
- <label className="text-sm text-[var(--color-text-muted)]">
2443
- Environment Variables / Credentials
2444
- </label>
2445
- <button
2446
- onClick={addEnvVar}
2447
- className="text-xs text-[var(--color-accent)] hover:text-[var(--color-accent-hover)] transition"
2448
- >
2449
- + Add Variable
2450
- </button>
2451
- </div>
2452
-
2453
- {envVars.length === 0 && (
2454
- <p className="text-xs text-[var(--color-text-faint)] bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded p-3">
2455
- No environment variables configured.
2456
- </p>
2457
- )}
2458
-
2459
- {envVars.length > 0 && (
2460
- <div className="space-y-2">
2461
- {envVars.map((env, index) => (
2462
- <div key={index} className="flex gap-2">
2463
- <input
2464
- type="text"
2465
- value={env.key}
2466
- onChange={e => updateEnvVar(index, "key", e.target.value)}
2467
- placeholder="KEY"
2468
- className="w-1/3 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-2 py-1.5 text-sm font-mono focus:outline-none focus:border-[var(--color-accent)]"
2469
- />
2470
- <input
2471
- type="password"
2472
- value={env.value}
2473
- onChange={e => updateEnvVar(index, "value", e.target.value)}
2474
- placeholder="value"
2475
- className="flex-1 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-2 py-1.5 text-sm font-mono focus:outline-none focus:border-[var(--color-accent)]"
2476
- />
2477
- <button
2478
- onClick={() => removeEnvVar(index)}
2479
- className="text-[var(--color-text-muted)] hover:text-red-400 px-2 transition"
2480
- >
2481
-
2482
- </button>
2483
- </div>
2484
- ))}
2485
- </div>
2486
- )}
2487
-
2488
- <p className="text-xs text-[var(--color-text-faint)] mt-2">
2489
- {server.status === "running" ? "Server will be automatically restarted to apply changes." : "Changes will take effect when the server is started."}
2490
- </p>
2491
- </div>
2492
- )}
2493
-
2494
- {error && <p className="text-red-400 text-sm">{error}</p>}
2495
- </div>
2496
-
2497
- <div className="p-4 border-t border-[var(--color-border)] flex justify-end gap-2 sticky bottom-0 bg-[var(--color-surface)]">
2498
- <button
2499
- onClick={onClose}
2500
- className="px-4 py-2 border border-[var(--color-border-light)] hover:border-[var(--color-text-muted)] rounded transition"
2501
- >
2502
- Cancel
2503
- </button>
2504
- <button
2505
- onClick={handleSave}
2506
- disabled={saving || !name.trim()}
2507
- className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black rounded font-medium transition disabled:opacity-50"
2508
- >
2509
- {saving ? "Saving..." : "Save Changes"}
2510
- </button>
2511
- </div>
2512
- </div>
2513
- </div>
2514
- );
2515
- }