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(