@myrialabs/clopen 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/backend/index.ts +25 -1
- package/backend/lib/auth/auth-service.ts +484 -0
- package/backend/lib/auth/index.ts +4 -0
- package/backend/lib/auth/permissions.ts +63 -0
- package/backend/lib/auth/rate-limiter.ts +145 -0
- package/backend/lib/auth/tokens.ts +53 -0
- package/backend/lib/chat/stream-manager.ts +4 -1
- package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
- package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
- package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
- package/backend/lib/database/migrations/index.ts +21 -0
- package/backend/lib/database/queries/auth-queries.ts +201 -0
- package/backend/lib/database/queries/index.ts +2 -1
- package/backend/lib/database/queries/session-queries.ts +13 -0
- package/backend/lib/database/queries/snapshot-queries.ts +1 -1
- package/backend/lib/engine/adapters/opencode/server.ts +9 -1
- package/backend/lib/engine/adapters/opencode/stream.ts +175 -1
- package/backend/lib/mcp/config.ts +13 -18
- package/backend/lib/mcp/index.ts +9 -0
- package/backend/lib/mcp/remote-server.ts +132 -0
- package/backend/lib/mcp/servers/helper.ts +49 -3
- package/backend/lib/mcp/servers/index.ts +3 -2
- package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
- package/backend/lib/preview/browser/browser-pool.ts +73 -176
- package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
- package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
- package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
- package/backend/lib/snapshot/helpers.ts +22 -49
- package/backend/lib/snapshot/snapshot-service.ts +148 -83
- package/backend/lib/utils/ws.ts +65 -1
- package/backend/ws/auth/index.ts +17 -0
- package/backend/ws/auth/invites.ts +84 -0
- package/backend/ws/auth/login.ts +269 -0
- package/backend/ws/auth/status.ts +41 -0
- package/backend/ws/auth/users.ts +32 -0
- package/backend/ws/chat/stream.ts +13 -0
- package/backend/ws/engine/claude/accounts.ts +3 -1
- package/backend/ws/engine/utils.ts +38 -6
- package/backend/ws/index.ts +4 -4
- package/backend/ws/preview/browser/interact.ts +27 -5
- package/backend/ws/snapshot/restore.ts +111 -12
- package/backend/ws/snapshot/timeline.ts +56 -29
- package/bin/clopen.ts +56 -1
- package/bun.lock +113 -51
- package/frontend/App.svelte +47 -29
- package/frontend/lib/components/auth/InvitePage.svelte +215 -0
- package/frontend/lib/components/auth/LoginPage.svelte +129 -0
- package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +1 -2
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +2 -2
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +4 -4
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -19
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +15 -3
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +30 -19
- package/frontend/lib/components/checkpoint/timeline/types.ts +4 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
- package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
- package/frontend/lib/components/git/CommitForm.svelte +6 -4
- package/frontend/lib/components/history/HistoryModal.svelte +1 -1
- package/frontend/lib/components/history/HistoryView.svelte +1 -1
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
- package/frontend/lib/components/settings/SettingsView.svelte +21 -7
- package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
- package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
- package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
- package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
- package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
- package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
- package/frontend/lib/components/workspace/PanelHeader.svelte +1 -1
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
- package/frontend/lib/stores/core/sessions.svelte.ts +15 -1
- package/frontend/lib/stores/features/auth.svelte.ts +296 -0
- package/frontend/lib/stores/features/settings.svelte.ts +53 -9
- package/frontend/lib/stores/features/user.svelte.ts +26 -68
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
- package/frontend/lib/stores/ui/update.svelte.ts +2 -14
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -4
- package/package.json +8 -6
- package/shared/types/stores/settings.ts +16 -2
- package/shared/utils/logger.ts +1 -0
- package/shared/utils/ws-client.ts +30 -13
- package/shared/utils/ws-server.ts +42 -4
- package/backend/lib/mcp/stdio-server.ts +0 -103
- package/backend/ws/mcp/index.ts +0 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myrialabs/clopen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
|
|
5
5
|
"author": "Myria Labs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"@types/bun": "^1.2.18",
|
|
63
63
|
"@types/node": "^24.0.14",
|
|
64
64
|
"@types/qrcode": "^1.5.6",
|
|
65
|
+
"@types/random-useragent": "^0.3.3",
|
|
65
66
|
"concurrently": "^9.2.1",
|
|
66
67
|
"eslint": "^9.31.0",
|
|
67
68
|
"eslint-plugin-svelte": "^3.10.1",
|
|
@@ -74,14 +75,14 @@
|
|
|
74
75
|
"vite": "^7.0.4"
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
78
|
-
"@anthropic-ai/sdk": "
|
|
78
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.63",
|
|
79
|
+
"@anthropic-ai/sdk": "0.78.0",
|
|
80
|
+
"@opencode-ai/sdk": "1.2.15",
|
|
79
81
|
"@elysiajs/cors": "^1.4.0",
|
|
80
82
|
"@iconify-json/lucide": "^1.2.57",
|
|
81
83
|
"@iconify-json/material-icon-theme": "^1.2.16",
|
|
82
84
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
83
85
|
"@monaco-editor/loader": "^1.5.0",
|
|
84
|
-
"@opencode-ai/sdk": "^1.2.15",
|
|
85
86
|
"@xterm/addon-clipboard": "^0.2.0",
|
|
86
87
|
"@xterm/addon-fit": "^0.11.0",
|
|
87
88
|
"@xterm/addon-ligatures": "^0.10.0",
|
|
@@ -96,8 +97,9 @@
|
|
|
96
97
|
"marked": "^16.1.1",
|
|
97
98
|
"monaco-editor": "^0.52.2",
|
|
98
99
|
"nanoid": "^5.1.6",
|
|
99
|
-
"puppeteer": "^24.
|
|
100
|
-
"puppeteer-
|
|
100
|
+
"puppeteer": "^24.37.5",
|
|
101
|
+
"puppeteer-extra": "^3.3.6",
|
|
102
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
101
103
|
"qrcode": "^1.5.4"
|
|
102
104
|
},
|
|
103
105
|
"trustedDependencies": [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EngineType } from '$shared/types/engine';
|
|
2
2
|
|
|
3
|
+
/** Per-user settings (stored per user) */
|
|
3
4
|
export interface AppSettings {
|
|
4
5
|
selectedEngine: EngineType;
|
|
5
6
|
selectedModel: string;
|
|
@@ -10,10 +11,23 @@ export interface AppSettings {
|
|
|
10
11
|
soundNotifications: boolean;
|
|
11
12
|
pushNotifications: boolean;
|
|
12
13
|
layoutPresetVisibility: Record<string, boolean>;
|
|
13
|
-
/** Restrict folder browser to only these base paths. Empty = no restriction. */
|
|
14
|
-
allowedBasePaths: string[];
|
|
15
14
|
/** Base font size in pixels (10–20). Default: 13. */
|
|
16
15
|
fontSize: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Authentication mode */
|
|
19
|
+
export type AuthMode = 'none' | 'required';
|
|
20
|
+
|
|
21
|
+
/** System-wide settings (admin-only, shared across all users) */
|
|
22
|
+
export interface SystemSettings {
|
|
23
|
+
/** Authentication mode: 'none' = single user no login, 'required' = multi-user with login. Default: 'required'. */
|
|
24
|
+
authMode: AuthMode;
|
|
25
|
+
/** Whether the initial setup wizard has been completed. Default: false. */
|
|
26
|
+
onboardingComplete: boolean;
|
|
27
|
+
/** Restrict folder browser to only these base paths. Empty = no restriction. */
|
|
28
|
+
allowedBasePaths: string[];
|
|
17
29
|
/** Automatically update to the latest version when available. Default: false. */
|
|
18
30
|
autoUpdate: boolean;
|
|
31
|
+
/** Session lifetime in days. Default: 30. */
|
|
32
|
+
sessionLifetimeDays: number;
|
|
19
33
|
}
|
package/shared/utils/logger.ts
CHANGED
|
@@ -221,6 +221,9 @@ export class WSClient<TAPI extends { client: any; server: any }> {
|
|
|
221
221
|
projectId: null
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
+
/** Session token for re-authentication on reconnection */
|
|
225
|
+
private sessionToken: string | null = null;
|
|
226
|
+
|
|
224
227
|
/** Pending context sync (for reconnection) */
|
|
225
228
|
private pendingContextSync = false;
|
|
226
229
|
|
|
@@ -272,8 +275,18 @@ export class WSClient<TAPI extends { client: any; server: any }> {
|
|
|
272
275
|
this.reconnectAttempts = 0;
|
|
273
276
|
this.options.onStatusChange?.('connected', 0);
|
|
274
277
|
|
|
275
|
-
//
|
|
276
|
-
if (this.
|
|
278
|
+
// Re-authenticate on reconnection using stored session token
|
|
279
|
+
if (this.sessionToken) {
|
|
280
|
+
try {
|
|
281
|
+
await this.http('auth:login' as any, { token: this.sessionToken } as any);
|
|
282
|
+
debug.log('websocket', 'Re-authenticated after reconnection');
|
|
283
|
+
} catch (err) {
|
|
284
|
+
debug.error('websocket', 'Failed to re-authenticate on reconnection:', err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Sync project context on reconnection (userId set by auth above)
|
|
289
|
+
if (this.context.projectId) {
|
|
277
290
|
try {
|
|
278
291
|
await this.syncContext();
|
|
279
292
|
debug.log('websocket', 'Context synced after reconnection');
|
|
@@ -570,18 +583,23 @@ export class WSClient<TAPI extends { client: any; server: any }> {
|
|
|
570
583
|
private contextSyncPromise: Promise<void> | null = null;
|
|
571
584
|
|
|
572
585
|
/**
|
|
573
|
-
* Set
|
|
574
|
-
*
|
|
586
|
+
* Set session token for reconnection auth.
|
|
587
|
+
* Called by auth store after successful login.
|
|
588
|
+
*/
|
|
589
|
+
setSessionToken(token: string | null): void {
|
|
590
|
+
this.sessionToken = token;
|
|
591
|
+
debug.log('websocket', 'Session token set for reconnection auth');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Set user context locally (for reconnection tracking).
|
|
596
|
+
* userId is now set server-side by auth handlers — this only updates the local cache.
|
|
575
597
|
*/
|
|
576
598
|
async setUser(userId: string | null): Promise<void> {
|
|
577
599
|
if (this.context.userId === userId) return;
|
|
578
|
-
|
|
579
600
|
this.context.userId = userId;
|
|
580
|
-
debug.log('websocket', 'Context: user set to', userId);
|
|
581
|
-
|
|
582
|
-
if (this.isConnected) {
|
|
583
|
-
await this.syncContext();
|
|
584
|
-
}
|
|
601
|
+
debug.log('websocket', 'Context: user set locally to', userId);
|
|
602
|
+
// Note: userId is NOT synced via ws:set-context — it's set server-side by auth handlers
|
|
585
603
|
}
|
|
586
604
|
|
|
587
605
|
/**
|
|
@@ -679,16 +697,15 @@ export class WSClient<TAPI extends { client: any; server: any }> {
|
|
|
679
697
|
// Register response listener
|
|
680
698
|
unsubResponse = this.on('ws:set-context:response' as any, handleResponse);
|
|
681
699
|
|
|
682
|
-
// Send context sync request
|
|
700
|
+
// Send context sync request (only projectId — userId is set server-side by auth)
|
|
683
701
|
this.emit('ws:set-context' as any, {
|
|
684
702
|
requestId,
|
|
685
703
|
data: {
|
|
686
|
-
userId: this.context.userId,
|
|
687
704
|
projectId: this.context.projectId
|
|
688
705
|
}
|
|
689
706
|
} as any);
|
|
690
707
|
|
|
691
|
-
debug.log('websocket', 'Context sync sent:', this.context);
|
|
708
|
+
debug.log('websocket', 'Context sync sent: projectId=', this.context.projectId);
|
|
692
709
|
});
|
|
693
710
|
}
|
|
694
711
|
|
|
@@ -279,11 +279,23 @@ export class WSRouter<
|
|
|
279
279
|
private httpRoutes = new Map<string, HTTPRoute>();
|
|
280
280
|
private eventSchemas = new Map<string, TSchema>();
|
|
281
281
|
|
|
282
|
+
/** Optional auth middleware — called before every route handler */
|
|
283
|
+
private authMiddleware: ((conn: WSConnection, action: string) => Promise<{ allowed: boolean; error?: string }>) | null = null;
|
|
284
|
+
|
|
282
285
|
constructor() {
|
|
283
286
|
// Register built-in context management route
|
|
284
287
|
this.registerContextHandler();
|
|
285
288
|
}
|
|
286
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Set an auth middleware function that gates all route handlers.
|
|
292
|
+
* The middleware receives the connection and action, and returns { allowed, error? }.
|
|
293
|
+
* If not allowed, the handler is not called and an auth:error event is sent to the client.
|
|
294
|
+
*/
|
|
295
|
+
setAuthMiddleware(fn: (conn: WSConnection, action: string) => Promise<{ allowed: boolean; error?: string }>): void {
|
|
296
|
+
this.authMiddleware = fn;
|
|
297
|
+
}
|
|
298
|
+
|
|
287
299
|
/**
|
|
288
300
|
* Register built-in ws:set-context handler
|
|
289
301
|
* Allows frontend to sync user/project context
|
|
@@ -292,7 +304,6 @@ export class WSRouter<
|
|
|
292
304
|
this.httpRoutes.set('ws:set-context', {
|
|
293
305
|
action: 'ws:set-context',
|
|
294
306
|
dataSchema: t.Object({
|
|
295
|
-
userId: t.Optional(t.Union([t.String(), t.Null()])),
|
|
296
307
|
projectId: t.Optional(t.Union([t.String(), t.Null()]))
|
|
297
308
|
}),
|
|
298
309
|
responseSchema: t.Object({
|
|
@@ -303,9 +314,8 @@ export class WSRouter<
|
|
|
303
314
|
// Import ws server to update context
|
|
304
315
|
const { ws: wsServer } = await import('$backend/lib/utils/ws');
|
|
305
316
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
317
|
+
// userId is set exclusively by auth handlers (auth:login, auth:setup, auth:accept-invite)
|
|
318
|
+
// ws:set-context only handles projectId
|
|
309
319
|
if (data.projectId !== undefined) {
|
|
310
320
|
wsServer.setProject(conn, data.projectId);
|
|
311
321
|
}
|
|
@@ -576,6 +586,34 @@ export class WSRouter<
|
|
|
576
586
|
return;
|
|
577
587
|
}
|
|
578
588
|
|
|
589
|
+
// ═══ AUTH GATE ═══
|
|
590
|
+
if (this.authMiddleware) {
|
|
591
|
+
const authResult = await this.authMiddleware(conn, action);
|
|
592
|
+
if (!authResult.allowed) {
|
|
593
|
+
try {
|
|
594
|
+
// If this is an HTTP route, send error as HTTP-style response
|
|
595
|
+
// so ws.http() on the client receives it instead of timing out
|
|
596
|
+
if (this.httpRoutes.has(action)) {
|
|
597
|
+
const requestId = payload?.requestId;
|
|
598
|
+
conn.send(JSON.stringify({
|
|
599
|
+
action: `${action}:response`,
|
|
600
|
+
payload: { success: false, error: authResult.error, requestId }
|
|
601
|
+
}));
|
|
602
|
+
} else {
|
|
603
|
+
conn.send(JSON.stringify({
|
|
604
|
+
action: 'auth:error',
|
|
605
|
+
payload: { error: authResult.error, blockedAction: action }
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
} catch {
|
|
609
|
+
// Connection may be closed
|
|
610
|
+
}
|
|
611
|
+
debug.warn('websocket', `Auth blocked: ${action} — ${authResult.error}`);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// ═══ END AUTH GATE ═══
|
|
616
|
+
|
|
579
617
|
// Check if this is an HTTP route
|
|
580
618
|
const httpRoute = this.httpRoutes.get(action);
|
|
581
619
|
if (httpRoute) {
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* MCP Stdio Server for Open Code
|
|
4
|
-
*
|
|
5
|
-
* Standalone subprocess that exposes our custom MCP tools via stdio transport.
|
|
6
|
-
* Open Code spawns this process and communicates over stdin/stdout (JSON-RPC 2.0).
|
|
7
|
-
*
|
|
8
|
-
* Tool definitions (schema, description) are loaded from the SAME source as
|
|
9
|
-
* Claude Code — the `serverMetadata` registry in `./servers/index.ts`.
|
|
10
|
-
*
|
|
11
|
-
* ALL tool calls are proxied to the main Clopen server via WSClient bridge,
|
|
12
|
-
* because handlers need in-process access to browser instances, project context, etc.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
-
import { WSClient } from '$shared/utils/ws-client';
|
|
18
|
-
import { serverMetadata } from './servers/index';
|
|
19
|
-
import { mcpServers } from './config';
|
|
20
|
-
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// WebSocket Bridge — proxies tool calls to the main Clopen server via WSClient
|
|
23
|
-
// ============================================================================
|
|
24
|
-
|
|
25
|
-
const BRIDGE_PORT = process.env.CLOPEN_PORT || '9141';
|
|
26
|
-
const WS_URL = `ws://localhost:${BRIDGE_PORT}/ws`;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* WSClient instance for communicating with the main Clopen server.
|
|
30
|
-
* Uses the same protocol as the frontend (JSON messages with action/payload).
|
|
31
|
-
*/
|
|
32
|
-
const wsClient = new WSClient<any>(WS_URL, {
|
|
33
|
-
autoReconnect: true,
|
|
34
|
-
maxReconnectAttempts: 10,
|
|
35
|
-
reconnectDelay: 1000,
|
|
36
|
-
maxReconnectDelay: 10000,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Call a tool handler on the main Clopen server via the WS bridge.
|
|
41
|
-
* Uses WSClient.http() which follows the standard request-response pattern.
|
|
42
|
-
*/
|
|
43
|
-
async function callBridge(
|
|
44
|
-
serverName: string,
|
|
45
|
-
toolName: string,
|
|
46
|
-
args: Record<string, unknown>
|
|
47
|
-
): Promise<{ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean }> {
|
|
48
|
-
try {
|
|
49
|
-
return await wsClient.http('mcp:execute' as any, {
|
|
50
|
-
server: serverName,
|
|
51
|
-
tool: toolName,
|
|
52
|
-
args,
|
|
53
|
-
} as any, 30000);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
56
|
-
return {
|
|
57
|
-
content: [{ type: 'text', text: `Bridge connection failed: ${msg}` }],
|
|
58
|
-
isError: true,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ============================================================================
|
|
64
|
-
// Build MCP server from existing server metadata (single source of truth)
|
|
65
|
-
// ============================================================================
|
|
66
|
-
|
|
67
|
-
const server = new McpServer({
|
|
68
|
-
name: 'clopen-mcp',
|
|
69
|
-
version: '1.0.0',
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Iterate over all configured servers and register their enabled tools
|
|
73
|
-
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
74
|
-
if (!serverConfig.enabled) continue;
|
|
75
|
-
|
|
76
|
-
// Get tool definitions from the metadata registry
|
|
77
|
-
const meta = (serverMetadata as Record<string, { toolDefs: Record<string, { description: string; schema: Record<string, any> }> }>)[serverName];
|
|
78
|
-
if (!meta) continue;
|
|
79
|
-
|
|
80
|
-
for (const toolName of serverConfig.tools) {
|
|
81
|
-
const toolDef = meta.toolDefs[toolName];
|
|
82
|
-
if (!toolDef) continue;
|
|
83
|
-
|
|
84
|
-
// Register tool with the same schema/description, but handler goes through bridge
|
|
85
|
-
server.registerTool(
|
|
86
|
-
toolName,
|
|
87
|
-
{
|
|
88
|
-
description: toolDef.description,
|
|
89
|
-
inputSchema: toolDef.schema,
|
|
90
|
-
},
|
|
91
|
-
async (args: Record<string, unknown>) => {
|
|
92
|
-
return await callBridge(serverName, toolName, args) as any;
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ============================================================================
|
|
99
|
-
// Connect via stdio transport
|
|
100
|
-
// ============================================================================
|
|
101
|
-
|
|
102
|
-
const transport = new StdioServerTransport();
|
|
103
|
-
await server.connect(transport);
|
package/backend/ws/mcp/index.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Bridge — WebSocket HTTP-like route for the stdio MCP server
|
|
3
|
-
*
|
|
4
|
-
* Provides a WS route that the MCP stdio server (spawned by Open Code)
|
|
5
|
-
* uses to execute tool handlers in-process.
|
|
6
|
-
*
|
|
7
|
-
* Handlers are loaded from the SAME serverMetadata registry —
|
|
8
|
-
* no duplication of tool names or handler imports.
|
|
9
|
-
*
|
|
10
|
-
* Route: mcp:execute (WS http-like request-response)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { createRouter } from '$shared/utils/ws-server';
|
|
14
|
-
import { t } from 'elysia';
|
|
15
|
-
import { debug } from '$shared/utils/logger';
|
|
16
|
-
import { serverMetadata } from '../../lib/mcp/servers/index';
|
|
17
|
-
import { mcpServers } from '../../lib/mcp/config';
|
|
18
|
-
|
|
19
|
-
export const mcpRouter = createRouter()
|
|
20
|
-
.http('mcp:execute', {
|
|
21
|
-
data: t.Object({
|
|
22
|
-
server: t.String(),
|
|
23
|
-
tool: t.String(),
|
|
24
|
-
args: t.Record(t.String(), t.Unknown()),
|
|
25
|
-
}),
|
|
26
|
-
response: t.Any(),
|
|
27
|
-
}, async ({ data }) => {
|
|
28
|
-
const { server, tool, args } = data;
|
|
29
|
-
|
|
30
|
-
debug.log('mcp', `🌉 Bridge: ${server}/${tool}`);
|
|
31
|
-
|
|
32
|
-
// Validate server exists and is enabled
|
|
33
|
-
const serverConfig = mcpServers[server];
|
|
34
|
-
if (!serverConfig?.enabled) {
|
|
35
|
-
return {
|
|
36
|
-
content: [{ type: 'text', text: `Unknown or disabled MCP server: ${server}` }],
|
|
37
|
-
isError: true,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Get handler from the single-source metadata registry
|
|
42
|
-
const meta = (serverMetadata as Record<string, { toolDefs: Record<string, { handler: (args: any) => Promise<any> }> }>)[server];
|
|
43
|
-
const toolDef = meta?.toolDefs[tool];
|
|
44
|
-
if (!toolDef) {
|
|
45
|
-
return {
|
|
46
|
-
content: [{ type: 'text', text: `Unknown tool: ${tool} in server ${server}` }],
|
|
47
|
-
isError: true,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
return await toolDef.handler(args);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
55
|
-
debug.error('mcp', `🌉 Bridge error: ${server}/${tool}: ${errorMessage}`);
|
|
56
|
-
return {
|
|
57
|
-
content: [{ type: 'text', text: `Tool execution error: ${errorMessage}` }],
|
|
58
|
-
isError: true,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
});
|