feat: Add WebMCP token polling for automatic browser connection
- Add token API endpoint to server for storing/retrieving pending tokens - Add token polling in frontend to auto-detect and connect with tokens - Add xterm dependencies for terminal functionality - Add terminal page support in toolRegistry
This commit is contained in:
@@ -25,7 +25,18 @@
|
|||||||
"mcp__agent-ui__localhost_3000-render_html",
|
"mcp__agent-ui__localhost_3000-render_html",
|
||||||
"mcp__agent-ui__localhost_4100-navigate_to",
|
"mcp__agent-ui__localhost_4100-navigate_to",
|
||||||
"mcp__agent-ui__localhost_4100-get_design_tokens",
|
"mcp__agent-ui__localhost_4100-get_design_tokens",
|
||||||
"mcp__agent-ui__localhost_4100-set_theme_variable"
|
"mcp__agent-ui__localhost_4100-set_theme_variable",
|
||||||
|
"mcp__agent-ui__localhost_4100-list_available_tools",
|
||||||
|
"mcp__agent-ui__localhost_4100-switch_theme",
|
||||||
|
"mcp__agent-ui__localhost_4100-set_default_theme",
|
||||||
|
"mcp__agent-ui__localhost_4100-save_theme",
|
||||||
|
"mcp__agent-ui___webmcp_browser-info",
|
||||||
|
"mcp__agent-ui__localhost_4100-render_vue_component",
|
||||||
|
"Bash(bun remove:*)",
|
||||||
|
"Bash(bun add:*)",
|
||||||
|
"mcp__agent-ui__localhost_4100-confetti",
|
||||||
|
"mcp__agent-ui__localhost_4100-get_current_page",
|
||||||
|
"mcp__agent-ui___webmcp_server-info"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": true,
|
"enableAllProjectMcpServers": true,
|
||||||
|
|||||||
24
frontend/package-lock.json
generated
24
frontend/package-lock.json
generated
@@ -9,6 +9,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
@@ -2103,6 +2106,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/addon-web-links": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/xterm": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"addons/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
"@nucleoriofrio/webmcp": "git+https://gitea.nucleoriofrio.com/nucleo000/webmcp.git",
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import StatusBar from './components/StatusBar.vue'
|
import StatusBar from './components/StatusBar.vue'
|
||||||
import Toolbar from './components/Toolbar.vue'
|
import Toolbar from './components/Toolbar.vue'
|
||||||
import ComponentsDropdown from './components/ComponentsDropdown.vue'
|
import ComponentsDropdown from './components/ComponentsDropdown.vue'
|
||||||
import FloatingTerminal from './components/FloatingTerminal.vue'
|
import FloatingTerminal from './components/FloatingTerminal.vue'
|
||||||
import { initWebMCP } from './services/webmcp'
|
import { initWebMCP, getWebMCP, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp'
|
||||||
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
||||||
|
import { useCanvasStore } from './stores/canvas'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const showTerminal = ref(false)
|
const showTerminal = ref(false)
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal'
|
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal'
|
||||||
|
|
||||||
@@ -24,6 +26,22 @@ onMounted(async () => {
|
|||||||
// Initialize tools for current page (handles refresh)
|
// Initialize tools for current page (handles refresh)
|
||||||
const currentPage = (route.name as string) || 'canvas'
|
const currentPage = (route.name as string) || 'canvas'
|
||||||
initToolsOnRefresh(currentPage as PageName)
|
initToolsOnRefresh(currentPage as PageName)
|
||||||
|
|
||||||
|
// Start polling for token 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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopTokenPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for route changes and update tools
|
// Watch for route changes and update tools
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
SOURCE_CODE_TOOLS
|
SOURCE_CODE_TOOLS
|
||||||
} from './tools/sourceCodeTools'
|
} from './tools/sourceCodeTools'
|
||||||
|
|
||||||
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source'
|
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal'
|
||||||
|
|
||||||
interface PageToolSet {
|
interface PageToolSet {
|
||||||
register: () => void
|
register: () => void
|
||||||
@@ -105,6 +105,11 @@ const pageTools: Record<PageName, PageToolSet> = {
|
|||||||
register: registerSourceCodeTools,
|
register: registerSourceCodeTools,
|
||||||
unregister: unregisterSourceCodeTools,
|
unregister: unregisterSourceCodeTools,
|
||||||
toolNames: SOURCE_CODE_TOOLS
|
toolNames: SOURCE_CODE_TOOLS
|
||||||
|
},
|
||||||
|
terminal: {
|
||||||
|
register: () => {},
|
||||||
|
unregister: () => {},
|
||||||
|
toolNames: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { useCanvasStore } from '../stores/canvas'
|
|||||||
let webmcpInstance: any = null
|
let webmcpInstance: any = null
|
||||||
const registeredTools = new Set<string>()
|
const registeredTools = new Set<string>()
|
||||||
|
|
||||||
|
const API_BASE = 'http://localhost:4101'
|
||||||
|
let tokenPollingInterval: number | null = null
|
||||||
|
|
||||||
export async function initWebMCP() {
|
export async function initWebMCP() {
|
||||||
if (webmcpInstance) return webmcpInstance
|
if (webmcpInstance) return webmcpInstance
|
||||||
|
|
||||||
@@ -95,3 +98,80 @@ export function getRegisteredTools(): string[] {
|
|||||||
export function isToolRegistered(name: string): boolean {
|
export function isToolRegistered(name: string): boolean {
|
||||||
return registeredTools.has(name)
|
return registeredTools.has(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Token polling functions
|
||||||
|
export async function checkForToken(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/webmcp-token`)
|
||||||
|
const data = await res.json()
|
||||||
|
return data.token || null
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearToken(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fetch(`${API_BASE}/api/webmcp-token`, { method: 'DELETE' })
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startTokenPolling(onToken: (token: string) => void, intervalMs: number = 2000) {
|
||||||
|
if (tokenPollingInterval) return
|
||||||
|
|
||||||
|
console.log('[WebMCP] Starting token polling...')
|
||||||
|
tokenPollingInterval = window.setInterval(async () => {
|
||||||
|
const token = await checkForToken()
|
||||||
|
if (token) {
|
||||||
|
console.log('[WebMCP] Token detected!')
|
||||||
|
stopTokenPolling()
|
||||||
|
onToken(token)
|
||||||
|
}
|
||||||
|
}, intervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopTokenPolling() {
|
||||||
|
if (tokenPollingInterval) {
|
||||||
|
window.clearInterval(tokenPollingInterval)
|
||||||
|
tokenPollingInterval = null
|
||||||
|
console.log('[WebMCP] Token polling stopped')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseToken(token: string): { server: string; token: string } | null {
|
||||||
|
try {
|
||||||
|
const decoded = atob(token)
|
||||||
|
return JSON.parse(decoded)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[WebMCP] Failed to parse token:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectWithToken(token: string): Promise<boolean> {
|
||||||
|
const parsed = parseToken(token)
|
||||||
|
if (!parsed) return false
|
||||||
|
|
||||||
|
console.log('[WebMCP] Connecting with token to:', parsed.server)
|
||||||
|
|
||||||
|
// Store token for webmcp to use
|
||||||
|
localStorage.setItem('webmcp_token', token)
|
||||||
|
|
||||||
|
// Clear the pending token from server
|
||||||
|
await clearToken()
|
||||||
|
|
||||||
|
// If webmcp is already initialized, try to reconnect
|
||||||
|
if (webmcpInstance && typeof webmcpInstance.connect === 'function') {
|
||||||
|
try {
|
||||||
|
await webmcpInstance.connect()
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[WebMCP] Failed to connect:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { spawn, type IPty } from '@skitee3000/bun-pty'
|
|||||||
const PORT_HTTP = 4101
|
const PORT_HTTP = 4101
|
||||||
const PORT_TERMINAL = 4103
|
const PORT_TERMINAL = 4103
|
||||||
|
|
||||||
|
// WebMCP token storage (in-memory, for passing token to browser)
|
||||||
|
let pendingWebMCPToken: { token: string; createdAt: Date } | null = null
|
||||||
|
|
||||||
// Terminal types
|
// Terminal types
|
||||||
interface TerminalSession {
|
interface TerminalSession {
|
||||||
id: string
|
id: string
|
||||||
@@ -299,6 +302,43 @@ Bun.serve({
|
|||||||
return Response.json({ status: 'ok', timestamp: new Date().toISOString() }, { headers: corsHeaders })
|
return Response.json({ status: 'ok', timestamp: new Date().toISOString() }, { headers: corsHeaders })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebMCP Token API - para pasar token al browser
|
||||||
|
if (url.pathname === '/api/webmcp-token') {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
if (pendingWebMCPToken) {
|
||||||
|
// Check if token is not expired (5 minutes)
|
||||||
|
const age = Date.now() - pendingWebMCPToken.createdAt.getTime()
|
||||||
|
if (age < 5 * 60 * 1000) {
|
||||||
|
return Response.json({
|
||||||
|
token: pendingWebMCPToken.token,
|
||||||
|
createdAt: pendingWebMCPToken.createdAt.toISOString()
|
||||||
|
}, { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
// Token expired
|
||||||
|
pendingWebMCPToken = null
|
||||||
|
}
|
||||||
|
return Response.json({ token: null }, { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const body = await req.json()
|
||||||
|
if (body.token) {
|
||||||
|
pendingWebMCPToken = {
|
||||||
|
token: body.token,
|
||||||
|
createdAt: new Date()
|
||||||
|
}
|
||||||
|
console.log('[WebMCP] Token received and stored')
|
||||||
|
return Response.json({ success: true }, { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
return Response.json({ error: 'Token required' }, { status: 400, headers: corsHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
pendingWebMCPToken = null
|
||||||
|
return Response.json({ success: true }, { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// API de Componentes Vue
|
// API de Componentes Vue
|
||||||
if (url.pathname === '/api/components') {
|
if (url.pathname === '/api/components') {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-ui-server",
|
"name": "agent-ui-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run index.ts",
|
"start": "bun run index.ts",
|
||||||
"dev": "bun --watch run index.ts"
|
"dev": "bun --watch run index.ts"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@skitee3000/bun-pty": "^0.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user