feat: Auto-connect WebMCP without agent intervention
- Add requestToken() and autoConnect() functions to request tokens directly from WebMCP server - Add /api/webmcp-request-token endpoint to proxy token requests (for HTTPS/Traefik) - Add webmcpHttp endpoint configuration for direct WebMCP HTTP API access - Update App.vue to auto-connect on load with polling fallback - Add kill-ports script to clear ports 4101, 4102, 4103, 4105 before starting
This commit is contained in:
2
frontend/package-lock.json
generated
2
frontend/package-lock.json
generated
@@ -1915,7 +1915,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@nucleoriofrio/webmcp": {
|
"node_modules/@nucleoriofrio/webmcp": {
|
||||||
"version": "0.2.0",
|
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.6.1",
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import FloatingTerminal from './components/FloatingTerminal.vue'
|
|||||||
import FloatingResponse from './components/FloatingResponse.vue'
|
import FloatingResponse from './components/FloatingResponse.vue'
|
||||||
import FloatingVoice from './components/FloatingVoice.vue'
|
import FloatingVoice from './components/FloatingVoice.vue'
|
||||||
import PwaInstallBanner from './components/PwaInstallBanner.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 { endpoints } from './config/endpoints'
|
||||||
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
||||||
import { setTerminalControls } from './services/tools/handlers/terminalHandlers'
|
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()
|
const webmcp = getWebMCP()
|
||||||
if (!webmcp?.isConnected) {
|
if (!webmcp?.isConnected) {
|
||||||
startTokenPolling(async (token) => {
|
// Try auto-connect (works in both dev and HTTPS via API proxy)
|
||||||
console.log('[App] Token received, connecting...')
|
const connected = await autoConnect()
|
||||||
const success = await connectWithToken(token)
|
if (connected) {
|
||||||
if (success) {
|
canvasStore.showNotification('WebMCP connected!', 'success')
|
||||||
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ function buildWsUrl(securePath: string, devPort: number): string {
|
|||||||
return `${wsProtocol}//${hostname}:${devPort}`
|
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
|
// Endpoint configuration
|
||||||
export const endpoints = {
|
export const endpoints = {
|
||||||
// Terminal WebSocket
|
// Terminal WebSocket
|
||||||
@@ -39,6 +47,9 @@ export const endpoints = {
|
|||||||
// WebMCP WebSocket
|
// WebMCP WebSocket
|
||||||
webmcp: buildWsUrl('/ws/mcp', 4102),
|
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 base URL (Vite proxy handles /api in dev)
|
||||||
api: '/api'
|
api: '/api'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useCanvasStore } from '../stores/canvas'
|
import { useCanvasStore } from '../stores/canvas'
|
||||||
import { endpoints, isSecure, wsProtocol, hostname } from '../config/endpoints'
|
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
|
let webmcpInstance: any = null
|
||||||
const registeredTools = new Set<string>()
|
const registeredTools = new Set<string>()
|
||||||
const eventUnsubscribers: Array<() => void> = []
|
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<string | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
export async function connectWithToken(token: string): Promise<boolean> {
|
||||||
if (!webmcpInstance) {
|
if (!webmcpInstance) {
|
||||||
console.error('[WebMCP] Instance not initialized')
|
console.error('[WebMCP] Instance not initialized')
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Dynamic canvas for Claude Code interaction",
|
"description": "Dynamic canvas for Claude Code interaction",
|
||||||
"scripts": {
|
"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:api": "cd server && bun --watch run index.ts",
|
||||||
"start:terminal": "cd server && bun run terminal.ts",
|
"start:terminal": "cd server && bun run terminal.ts",
|
||||||
"start:frontend": "cd frontend && bun run dev --host"
|
"start:frontend": "cd frontend && bun run dev --host"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { optionsResponse, notFoundResponse } from '../utils/cors'
|
import { optionsResponse, notFoundResponse } from '../utils/cors'
|
||||||
import { handleHistory } from './history'
|
import { handleHistory } from './history'
|
||||||
import { handleConfig, handleHealth } from './config'
|
import { handleConfig, handleHealth } from './config'
|
||||||
import { handleWebMCPToken } from './webmcp'
|
import { handleWebMCPToken, handleWebMCPRequestToken } from './webmcp'
|
||||||
import { handleComponents, handleComponentById, handleComponentUsage } from './components'
|
import { handleComponents, handleComponentById, handleComponentUsage } from './components'
|
||||||
import { handleThemes, handleActiveTheme, handleDesignTokens, handleThemeById, handleThemeExport } from './themes'
|
import { handleThemes, handleActiveTheme, handleDesignTokens, handleThemeById, handleThemeExport } from './themes'
|
||||||
import { handleCanvas, handleCanvasById, handleToolbarCanvas, handleDefaultCanvas, handleCanvasComponents, handleCanvasComponentById } from './canvas'
|
import { handleCanvas, handleCanvasById, handleToolbarCanvas, handleDefaultCanvas, handleCanvasComponents, handleCanvasComponentById } from './canvas'
|
||||||
@@ -38,12 +38,18 @@ export async function handleRequest(req: Request): Promise<Response> {
|
|||||||
if (res) return res
|
if (res) return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebMCP Token
|
// WebMCP Token (for polling - legacy)
|
||||||
if (path === '/api/webmcp-token') {
|
if (path === '/api/webmcp-token') {
|
||||||
const res = await handleWebMCPToken(req)
|
const res = await handleWebMCPToken(req)
|
||||||
if (res) return res
|
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)
|
// Claude Code status (thinking/idle)
|
||||||
if (path === '/api/claude-status') {
|
if (path === '/api/claude-status') {
|
||||||
const res = await handleClaudeStatus(req)
|
const res = await handleClaudeStatus(req)
|
||||||
|
|||||||
@@ -1,8 +1,41 @@
|
|||||||
import { jsonResponse, errorResponse } from '../utils/cors'
|
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)
|
// WebMCP token storage (in-memory)
|
||||||
let pendingWebMCPToken: { token: string; createdAt: Date } | null = null
|
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) {
|
export async function handleWebMCPToken(req: Request) {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
if (pendingWebMCPToken) {
|
if (pendingWebMCPToken) {
|
||||||
|
|||||||
Reference in New Issue
Block a user