diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d464865..ea55a05 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1915,7 +1915,7 @@ }, "node_modules/@nucleoriofrio/webmcp": { "version": "0.2.0", - "resolved": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git#cec5be355d67e0cf9049380ece74e9eac0e85f5e", + "resolved": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git#870207f15199369bc262d27ce0f90a27f2854be4", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ed1aed5..7aba9b5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,7 +10,7 @@ import FloatingTerminal from './components/FloatingTerminal.vue' import FloatingResponse from './components/FloatingResponse.vue' import FloatingVoice from './components/FloatingVoice.vue' import PwaInstallBanner from './components/PwaInstallBanner.vue' -import { initWebMCP, getWebMCP, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp' +import { initWebMCP, getWebMCP, autoConnect, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp' import { endpoints } from './config/endpoints' import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry' import { setTerminalControls } from './services/tools/handlers/terminalHandlers' @@ -322,16 +322,24 @@ onMounted(async () => { }) } - // Start polling for token if not connected + // Auto-connect to WebMCP if not connected const webmcp = getWebMCP() if (!webmcp?.isConnected) { - startTokenPolling(async (token) => { - console.log('[App] Token received, connecting...') - const success = await connectWithToken(token) - if (success) { - canvasStore.showNotification('WebMCP connected!', 'success') - } - }) + // Try auto-connect (works in both dev and HTTPS via API proxy) + const connected = await autoConnect() + if (connected) { + canvasStore.showNotification('WebMCP connected!', 'success') + } else { + // Fallback to polling (for older WebMCP versions without /token endpoint) + console.log('[App] Auto-connect failed, falling back to token polling...') + startTokenPolling(async (token) => { + console.log('[App] Token received, connecting...') + const success = await connectWithToken(token) + if (success) { + canvasStore.showNotification('WebMCP connected!', 'success') + } + }) + } } }) diff --git a/frontend/src/config/endpoints.ts b/frontend/src/config/endpoints.ts index d108d50..6afcf58 100644 --- a/frontend/src/config/endpoints.ts +++ b/frontend/src/config/endpoints.ts @@ -22,6 +22,14 @@ function buildWsUrl(securePath: string, devPort: number): string { return `${wsProtocol}//${hostname}:${devPort}` } +// Build HTTP URL for services +function buildHttpUrl(securePath: string, devPort: number): string { + if (isSecure) { + return `https://${hostname}${securePath}` + } + return `http://${hostname}:${devPort}` +} + // Endpoint configuration export const endpoints = { // Terminal WebSocket @@ -39,6 +47,9 @@ export const endpoints = { // WebMCP WebSocket webmcp: buildWsUrl('/ws/mcp', 4102), + // WebMCP HTTP API (for token requests) + webmcpHttp: buildHttpUrl('/mcp', 4102), + // API base URL (Vite proxy handles /api in dev) api: '/api' } diff --git a/frontend/src/services/webmcp.ts b/frontend/src/services/webmcp.ts index d87ddb3..42096d9 100644 --- a/frontend/src/services/webmcp.ts +++ b/frontend/src/services/webmcp.ts @@ -1,6 +1,9 @@ import { useCanvasStore } from '../stores/canvas' import { endpoints, isSecure, wsProtocol, hostname } from '../config/endpoints' +// WebMCP HTTP API base for direct token requests +const WEBMCP_HTTP = endpoints.webmcpHttp + let webmcpInstance: any = null const registeredTools = new Set() const eventUnsubscribers: Array<() => void> = [] @@ -271,6 +274,76 @@ export function parseToken(token: string): { server: string; token: string } | n } } +/** + * Request a new token directly from WebMCP server via HTTP API + * In development: calls WebMCP directly at port 4102 + * In production (HTTPS): calls Agent UI API which proxies to WebMCP + */ +export async function requestToken(): Promise { + try { + console.log('[WebMCP] Requesting token from server...') + + // In HTTPS mode, use Agent UI API as proxy (Traefik can't reach WebMCP directly) + // In development, call WebMCP directly + const url = isSecure ? `${API_BASE}/webmcp-request-token` : `${WEBMCP_HTTP}/token` + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }) + + if (!res.ok) { + console.error('[WebMCP] Failed to request token:', res.status) + return null + } + + // Check if response is JSON + const contentType = res.headers.get('content-type') + if (!contentType?.includes('application/json')) { + console.warn('[WebMCP] /token endpoint not available (server returned non-JSON)') + return null + } + + const data = await res.json() + if (data.success && data.token) { + console.log('[WebMCP] Token received from server') + return data.token + } + return null + } catch (e) { + // Network error or endpoint not available + console.warn('[WebMCP] /token endpoint not available:', (e as Error).message) + return null + } +} + +/** + * Auto-connect to WebMCP by requesting a token and connecting + * Returns true if connection was successful + */ +export async function autoConnect(): Promise { + if (!webmcpInstance) { + console.error('[WebMCP] Instance not initialized, call initWebMCP() first') + return false + } + + // Check if already connected + if (webmcpInstance.isConnected) { + console.log('[WebMCP] Already connected') + return true + } + + // Request token from server + const token = await requestToken() + if (!token) { + console.error('[WebMCP] Failed to get token') + return false + } + + // Connect with the token + return connectWithToken(token) +} + export async function connectWithToken(token: string): Promise { if (!webmcpInstance) { console.error('[WebMCP] Instance not initialized') diff --git a/package.json b/package.json index a977163..8f20cef 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "description": "Dynamic canvas for Claude Code interaction", "scripts": { - "start": "concurrently -n api,terminal,frontend -c blue,yellow,green \"cd server && bun --watch run index.ts\" \"cd server && bun run terminal.ts\" \"cd frontend && bun run dev --host\"", + "kill-ports": "node -e \"const {execSync} = require('child_process'); [4101,4102,4103,4105].forEach(p => { try { const pid = execSync('netstat -ano | findstr :' + p + ' | findstr LISTENING', {encoding:'utf8'}).split(/\\s+/).pop().trim(); if(pid) execSync('taskkill /PID ' + pid + ' /F', {stdio:'ignore'}); } catch(e){} }); console.log('Ports cleared');\"", + "start": "bun run kill-ports && concurrently -n api,terminal,frontend -c blue,yellow,green \"cd server && bun --watch run index.ts\" \"cd server && bun run terminal.ts\" \"cd frontend && bun run dev --host\"", "start:api": "cd server && bun --watch run index.ts", "start:terminal": "cd server && bun run terminal.ts", "start:frontend": "cd frontend && bun run dev --host" diff --git a/server/routes/index.ts b/server/routes/index.ts index 0a8081a..5fec066 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,7 +1,7 @@ import { optionsResponse, notFoundResponse } from '../utils/cors' import { handleHistory } from './history' import { handleConfig, handleHealth } from './config' -import { handleWebMCPToken } from './webmcp' +import { handleWebMCPToken, handleWebMCPRequestToken } from './webmcp' import { handleComponents, handleComponentById, handleComponentUsage } from './components' import { handleThemes, handleActiveTheme, handleDesignTokens, handleThemeById, handleThemeExport } from './themes' import { handleCanvas, handleCanvasById, handleToolbarCanvas, handleDefaultCanvas, handleCanvasComponents, handleCanvasComponentById } from './canvas' @@ -38,12 +38,18 @@ export async function handleRequest(req: Request): Promise { if (res) return res } - // WebMCP Token + // WebMCP Token (for polling - legacy) if (path === '/api/webmcp-token') { const res = await handleWebMCPToken(req) if (res) return res } + // WebMCP Request Token (direct request to WebMCP server) + if (path === '/api/webmcp-request-token') { + const res = await handleWebMCPRequestToken(req) + if (res) return res + } + // Claude Code status (thinking/idle) if (path === '/api/claude-status') { const res = await handleClaudeStatus(req) diff --git a/server/routes/webmcp.ts b/server/routes/webmcp.ts index 3fe7617..6f4880c 100644 --- a/server/routes/webmcp.ts +++ b/server/routes/webmcp.ts @@ -1,8 +1,41 @@ import { jsonResponse, errorResponse } from '../utils/cors' +// WebMCP server URL (localhost since they run on same machine) +const WEBMCP_URL = 'http://localhost:4102' + // WebMCP token storage (in-memory) let pendingWebMCPToken: { token: string; createdAt: Date } | null = null +// Request token directly from WebMCP server +export async function handleWebMCPRequestToken(req: Request) { + if (req.method !== 'POST') { + return errorResponse('Method not allowed', 405) + } + + try { + const res = await fetch(`${WEBMCP_URL}/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }) + + if (!res.ok) { + console.error('[WebMCP] Failed to request token from server:', res.status) + return errorResponse('Failed to request token', res.status) + } + + const data = await res.json() + if (data.success && data.token) { + console.log('[WebMCP] Token obtained from server') + return jsonResponse({ success: true, token: data.token }) + } + + return errorResponse('Invalid response from WebMCP', 500) + } catch (e) { + console.error('[WebMCP] Error requesting token:', e) + return errorResponse('WebMCP server not available', 503) + } +} + export async function handleWebMCPToken(req: Request) { if (req.method === 'GET') { if (pendingWebMCPToken) {