diff --git a/agent-node/src/agent_node/skills/shell_bridge.py b/agent-node/src/agent_node/skills/shell_bridge.py index 8b6a904..47ce665 100644 --- a/agent-node/src/agent_node/skills/shell_bridge.py +++ b/agent-node/src/agent_node/skills/shell_bridge.py @@ -146,7 +146,7 @@ clean_tail = ANSI_ESCAPE.sub('', sess["tail_buffer"]) - if end_fence_prefix in clean_tail or bracket_end_fence_prefix in clean_tail: + if end_fence_prefix in sess["tail_buffer"] or bracket_end_fence_prefix in sess["tail_buffer"]: try: is_bracket = bracket_end_fence_prefix in clean_tail active_end_prefix = bracket_end_fence_prefix if is_bracket else end_fence_prefix diff --git a/frontend/src/features/agents/components/AgentHarnessPage.js b/frontend/src/features/agents/components/AgentHarnessPage.js index 71602d4..0e4cedd 100644 --- a/frontend/src/features/agents/components/AgentHarnessPage.js +++ b/frontend/src/features/agents/components/AgentHarnessPage.js @@ -2,6 +2,7 @@ import { getAgents, getAgentTelemetry, updateAgentStatus, deployAgent, deleteAgent, getUserConfig, getUserAccessibleNodes, getAgentTriggers } from '../../../services/apiService'; import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; import BuddyAvatar from './BuddyAvatar'; +import { useWebMcp } from '../../../shared/components/WebMcpProvider'; // Polling interval in ms const POLLING_INTERVAL = 5000; @@ -16,11 +17,33 @@ }; export default function AgentHarnessPage({ onNavigate }) { + const { registerTool, unregisterTool } = useWebMcp(); const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showDeploy, setShowDeploy] = useState(false); + useEffect(() => { + registerTool({ + name: 'list_agents', + description: 'List all autonomous agents currently configured in the system.', + inputSchema: { type: 'object', properties: {} }, + handler: async () => { + return { agents: agents.map(a => ({ + id: a.id, + name: a.template?.name, + status: a.status, + node: a.mesh_node_id, + last_heartbeat: a.last_heartbeat + })) }; + } + }); + + return () => { + unregisterTool('list_agents'); + }; + }, [agents, registerTool, unregisterTool]); + const fetchAgents = async () => { try { const data = await getAgents(); diff --git a/frontend/src/features/nodes/pages/NodesPage.js b/frontend/src/features/nodes/pages/NodesPage.js index d17119b..c943ee1 100644 --- a/frontend/src/features/nodes/pages/NodesPage.js +++ b/frontend/src/features/nodes/pages/NodesPage.js @@ -7,7 +7,10 @@ import NodeTerminal from "../components/NodeTerminal"; import { FileSystemNavigator } from "../../../shared/components"; +import { useWebMcp } from '../../../shared/components/WebMcpProvider'; + const NodesPage = ({ user }) => { + const { registerTool, unregisterTool } = useWebMcp(); const [nodes, setNodes] = useState([]); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); @@ -37,6 +40,71 @@ const isAdmin = user?.role === 'admin'; + useEffect(() => { + // Register WebMCP tools + registerTool({ + name: 'list_nodes', + description: 'List all agent nodes in the mesh and their current status.', + inputSchema: { type: 'object', properties: {} }, + handler: async () => { + return { nodes: nodes.map(n => ({ + id: n.node_id, + name: n.display_name, + status: n.last_status, + is_active: n.is_active, + os: n.capabilities?.os + })) }; + } + }); + + registerTool({ + name: 'get_node_details', + description: 'Get full details for a specific agent node.', + inputSchema: { + type: 'object', + properties: { + node_id: { type: 'string', description: 'The unique ID of the node.' } + }, + required: ['node_id'] + }, + handler: async ({ node_id }) => { + const node = nodes.find(n => n.node_id === node_id); + if (!node) return { error: `Node ${node_id} not found.` }; + return { node }; + } + }); + + registerTool({ + name: 'toggle_node_active', + description: 'Enable or disable an agent node.', + inputSchema: { + type: 'object', + properties: { + node_id: { type: 'string', description: 'The unique ID of the node.' }, + active: { type: 'boolean', description: 'True to enable, False to disable.' } + }, + required: ['node_id', 'active'] + }, + handler: async ({ node_id, active }) => { + const node = nodes.find(n => n.node_id === node_id); + if (!node) return { error: `Node ${node_id} not found.` }; + try { + await adminUpdateNode(node_id, { is_active: active }); + fetchData(); + return { success: true, message: `Node ${node_id} ${active ? 'enabled' : 'disabled'}.` }; + } catch (err) { + return { error: err.message }; + } + } + }); + + return () => { + unregisterTool('list_nodes'); + unregisterTool('get_node_details'); + unregisterTool('toggle_node_active'); + }; + }, [nodes, registerTool, unregisterTool]); + const fetchData = useCallback(async () => { setLoading(true); try { diff --git a/frontend/src/features/skills/pages/SkillsPage.js b/frontend/src/features/skills/pages/SkillsPage.js index 81087ac..65af8d3 100644 --- a/frontend/src/features/skills/pages/SkillsPage.js +++ b/frontend/src/features/skills/pages/SkillsPage.js @@ -1,8 +1,10 @@ import React, { useState, useEffect, useMemo } from 'react'; import SkillsPageContent from '../components/SkillsPageContent'; import { getSkills, createSkill, updateSkill, deleteSkill, getSkillFiles, getSkillFileContent, saveSkillFile, deleteSkillFile } from '../../../services/apiService'; +import { useWebMcp } from '../../../shared/components/WebMcpProvider'; export default function SkillsPage({ user, Icon }) { + const { registerTool, unregisterTool } = useWebMcp(); const [skills, setSkills] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -12,6 +14,48 @@ const [errorModalMessage, setErrorModalMessage] = useState(null); const [confirmDeleteId, setConfirmDeleteId] = useState(null); + useEffect(() => { + registerTool({ + name: 'list_skills', + description: 'List all available skills (cortex node capabilities/folders) in the hub.', + inputSchema: { type: 'object', properties: {} }, + handler: async () => { + return { skills: skills.map(s => ({ + id: s.id, + name: s.name, + description: s.description, + type: s.skill_type, + is_system: s.is_system, + is_enabled: s.is_enabled + })) }; + } + }); + + registerTool({ + name: 'search_skills', + description: 'Search for skills based on name or description.', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'The search query string.' } + }, + required: ['query'] + }, + handler: async ({ query }) => { + const results = skills.filter(s => + s.name.toLowerCase().includes(query.toLowerCase()) || + (s.description || '').toLowerCase().includes(query.toLowerCase()) + ); + return { skills: results }; + } + }); + + return () => { + unregisterTool('list_skills'); + unregisterTool('search_skills'); + }; + }, [skills, registerTool, unregisterTool]); + const [isModalOpen, setIsModalOpen] = useState(false); const [editingSkill, setEditingSkill] = useState(null); diff --git a/frontend/src/index.js b/frontend/src/index.js index 111004f..64c5c7a 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -4,10 +4,14 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; +import { WebMcpProvider } from './shared/components/WebMcpProvider'; + const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + + + ); diff --git a/frontend/src/services/mcpService.js b/frontend/src/services/mcpService.js new file mode 100644 index 0000000..6ad6901 --- /dev/null +++ b/frontend/src/services/mcpService.js @@ -0,0 +1,62 @@ +/** + * mcpService.js + * + * Handles the registration of Model Context Protocol (MCP) tools for the browser. + * This allows AI agents to interact with the Hub UI as a set of structured tools. + */ + +class McpService { + constructor() { + this.isSupported = typeof window !== 'undefined' && !!window.navigator?.modelContext; + this.registeredTools = new Set(); + + if (!this.isSupported) { + console.warn('[MCP] WebMCP is not natively supported in this browser. Tools will not be exposed.'); + } + } + + /** + * Registers a tool with the browser's model context. + * @param {Object} tool - Tool definition following the MCP spec. + */ + registerTool(tool) { + if (!this.isSupported) return; + + try { + window.navigator.modelContext.registerTool(tool); + this.registeredTools.add(tool.name); + console.log(`[MCP] Registered tool: ${tool.name}`); + } catch (error) { + console.error(`[MCP] Failed to register tool ${tool.name}:`, error); + } + } + + /** + * Unregisters a tool. + * @param {string} toolName + */ + unregisterTool(toolName) { + if (!this.isSupported) return; + + try { + // Note: Native API might use different unregistration logic depending on the draft + if (window.navigator.modelContext.unregisterTool) { + window.navigator.modelContext.unregisterTool(toolName); + } + this.registeredTools.delete(toolName); + console.log(`[MCP] Unregistered tool: ${toolName}`); + } catch (error) { + console.error(`[MCP] Failed to unregister tool ${toolName}:`, error); + } + } + + /** + * Returns whether the protocol is active and supported. + */ + isActive() { + return this.isSupported; + } +} + +const mcpService = new McpService(); +export default mcpService; diff --git a/frontend/src/shared/components/Navbar.js b/frontend/src/shared/components/Navbar.js index 5f83d06..b0ee2c5 100644 --- a/frontend/src/shared/components/Navbar.js +++ b/frontend/src/shared/components/Navbar.js @@ -1,7 +1,9 @@ import React from 'react'; import { ReactComponent as Logo } from '../../logo.svg'; +import { useWebMcp } from './WebMcpProvider'; const Navbar = ({ isOpen, onToggle, onNavigate, onLogout, isLoggedIn, user, Icon }) => { + const { isMcpActive } = useWebMcp(); const navItems = [ { name: "Home", icon: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z", page: "home" }, { name: "Voice Chat", icon: "M12 1a3 3 0 0 1 3 3v7a3 3 0 1 1-6 0V4a3 3 0 0 1 3-3zm5 10a5 5 0 0 1-10 0H5a7 7 0 0 0 14 0h-2zm-5 11v-4h-2v4h2z", page: "voice-chat" }, @@ -69,6 +71,19 @@ {/* Bottom Section: User, Settings and Logout */}
+ {/* WebMCP Status Indicator */} +
+
+ {isOpen && ( +
+ WebMCP + + {isMcpActive ? 'Protocol Active' : 'Native API Missing'} + +
+ )} +
+ {/* Settings Button - Only shown to Admin */} {isLoggedIn && user?.role === "admin" && (
{ + const [isMcpActive, setIsMcpActive] = useState(false); + + useEffect(() => { + // Check if WebMCP is supported/active + if (mcpService.isActive()) { + setIsMcpActive(true); + console.log('[MCP] WebMCP Provider initialized and active.'); + } + }, []); + + const value = { + isMcpActive, + registerTool: (tool) => mcpService.registerTool(tool), + unregisterTool: (toolName) => mcpService.unregisterTool(toolName) + }; + + return ( + + {children} + + ); +}; + +export const useWebMcp = () => { + const context = useContext(WebMcpContext); + if (!context) { + throw new Error('useWebMcp must be used within a WebMcpProvider'); + } + return context; +}; diff --git a/reinstall_windows_agent.ps1 b/reinstall_windows_agent.ps1 new file mode 100644 index 0000000..cccf305 --- /dev/null +++ b/reinstall_windows_agent.ps1 @@ -0,0 +1,12 @@ +$taskName = "CortexAgent" +$pythonPath = "C:\CortexAgent\venv\Scripts\python.exe" +$mainPath = "C:\CortexAgent\src\agent_node\main.py" +$logPath = "C:\CortexAgent\agent.log" + +$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c `"$pythonPath $mainPath >> $logPath 2>&1`"" +$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable + +Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue +Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings +Start-ScheduledTask -TaskName $taskName diff --git a/start_agent.bat b/start_agent.bat new file mode 100644 index 0000000..692f498 --- /dev/null +++ b/start_agent.bat @@ -0,0 +1,4 @@ +@echo off +cd C:\CortexAgent +set PYTHONPATH=src +venv\Scripts\python.exe -u src\agent_node\main.py diff --git a/test_terminal.py b/test_terminal.py new file mode 100644 index 0000000..1e14258 --- /dev/null +++ b/test_terminal.py @@ -0,0 +1,47 @@ +import asyncio +import websockets +import json +import uuid + +async def test_terminal(): + user_id = "37471c66-9da0-42a5-8b00-8b2f5bb46baa" + node_id = "media-windows-server" + uri = f"wss://ai.jerxie.com/api/v1/nodes/stream/all?user_id={user_id}" + + async with websockets.connect(uri) as websocket: + print("Connected to Hub WebSocket") + + # 1. Wait for initial snapshot + msg = await websocket.recv() + print(f"Recv: {msg[:100]}...") + + # 2. Start a terminal session (implicitly by sending a character if session doesn't exist) + session_id = str(uuid.uuid4()) + print(f"Using Session ID: {session_id}") + + # 3. Send a command + cmd_msg = { + "action": "dispatch", + "node_id": node_id, + "session_id": session_id, + "command": "hostname" + } + await websocket.send(json.dumps(cmd_msg)) + print("Sent hostname command") + + # 4. Wait for output + while True: + try: + msg = await asyncio.wait_for(websocket.recv(), timeout=10.0) + data = json.loads(msg) + print(f"Event: {data.get('event')} | Data: {str(data.get('data'))[:100]}") + if data.get("event") in ("task_complete", "skill_event"): + if "hostname" in str(data.get("data")): + print("SUCCESS: Received hostname in output!") + return True + except asyncio.TimeoutError: + print("FAILED: Timeout waiting for output") + return False + +if __name__ == "__main__": + asyncio.run(test_terminal())