Skip to main content
Glama
tools.js32.6 kB
/** * MCP Tools Implementation * * Provides comprehensive planning tools for AI agents: * - Full CRUD operations on all entities * - Unified search across all scopes * - Batch operations for efficiency * - Rich context retrieval * - Text responses for Claude Desktop compatibility */ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); const apiClient = require('./api-client'); /** * Format JSON data as text for Claude Desktop */ function formatResponse(data) { // If data is an error object with a message, return just the message if (data && data.error) { return { isError: true, content: [ { type: "text", text: data.error } ] }; } // For successful responses, stringify the data return { content: [ { type: "text", text: JSON.stringify(data, null, 2) } ] }; } /** * Setup tools for the MCP server * @param {Server} server - MCP server instance */ function setupTools(server) { // Suppress console logs when not in debug mode if (process.env.NODE_ENV !== 'development') { // Silent mode for production } else { console.error('Setting up MCP tools...'); } // Handler for listing available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // ===== UNIFIED SEARCH TOOL ===== { name: "search", description: "Universal search tool for plans, nodes, and content", inputSchema: { type: "object", properties: { scope: { type: "string", description: "Search scope", enum: ["global", "plans", "plan", "node"], default: "global" }, scope_id: { type: "string", description: "Plan ID (if scope is 'plan') or Node ID (if scope is 'node')" }, query: { type: "string", description: "Search query" }, filters: { type: "object", description: "Optional filters", properties: { status: { type: "string", description: "Filter by status", enum: ["draft", "active", "completed", "archived", "not_started", "in_progress", "blocked"] }, type: { type: "string", description: "Filter by type", enum: ["plan", "node", "phase", "task", "milestone", "artifact", "log"] }, limit: { type: "integer", description: "Maximum number of results", default: 20 } } } }, required: ["query"] } }, // ===== PLAN MANAGEMENT TOOLS ===== { name: "list_plans", description: "List all plans or filter by status", inputSchema: { type: "object", properties: { status: { type: "string", description: "Optional filter by plan status", enum: ["draft", "active", "completed", "archived"] } } } }, { name: "create_plan", description: "Create a new plan", inputSchema: { type: "object", properties: { title: { type: "string", description: "Plan title" }, description: { type: "string", description: "Plan description" }, status: { type: "string", description: "Plan status", enum: ["draft", "active", "completed", "archived"], default: "draft" } }, required: ["title"] } }, { name: "update_plan", description: "Update an existing plan", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, title: { type: "string", description: "New plan title" }, description: { type: "string", description: "New plan description" }, status: { type: "string", description: "New plan status", enum: ["draft", "active", "completed", "archived"] } }, required: ["plan_id"] } }, { name: "delete_plan", description: "Delete a plan", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID to delete" } }, required: ["plan_id"] } }, // ===== NODE MANAGEMENT TOOLS ===== { name: "create_node", description: "Create a new node in a plan", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, parent_id: { type: "string", description: "Parent node ID (optional, defaults to root)" }, node_type: { type: "string", description: "Node type", enum: ["phase", "task", "milestone"] }, title: { type: "string", description: "Node title" }, description: { type: "string", description: "Node description" }, status: { type: "string", description: "Node status", enum: ["not_started", "in_progress", "completed", "blocked"], default: "not_started" }, context: { type: "string", description: "Additional context for the node" }, agent_instructions: { type: "string", description: "Instructions for AI agents working on this node" }, acceptance_criteria: { type: "string", description: "Criteria for node completion" }, due_date: { type: "string", description: "Due date (ISO format)" }, metadata: { type: "object", description: "Additional metadata" } }, required: ["plan_id", "node_type", "title"] } }, { name: "update_node", description: "Update a node's properties", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID" }, title: { type: "string", description: "New node title" }, description: { type: "string", description: "New node description" }, status: { type: "string", description: "New node status", enum: ["not_started", "in_progress", "completed", "blocked"] }, context: { type: "string", description: "New context" }, agent_instructions: { type: "string", description: "New agent instructions" }, acceptance_criteria: { type: "string", description: "New acceptance criteria" }, due_date: { type: "string", description: "New due date (ISO format)" }, metadata: { type: "object", description: "New metadata" } }, required: ["plan_id", "node_id"] } }, { name: "delete_node", description: "Delete a node and all its children", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID to delete" } }, required: ["plan_id", "node_id"] } }, { name: "move_node", description: "Move a node to a different parent or position", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID to move" }, parent_id: { type: "string", description: "New parent node ID" }, order_index: { type: "integer", description: "New position index" } }, required: ["plan_id", "node_id"] } }, { name: "get_node_context", description: "Get comprehensive context for a node including children, logs, and artifacts", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID" } }, required: ["plan_id", "node_id"] } }, { name: "get_node_ancestry", description: "Get the path from root to a specific node", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID" } }, required: ["plan_id", "node_id"] } }, // ===== LOGGING TOOLS (Replaces Comments) ===== { name: "add_log", description: "Add a log entry to a node (replaces comments)", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID" }, content: { type: "string", description: "Log content" }, log_type: { type: "string", description: "Type of log entry", enum: ["progress", "reasoning", "challenge", "decision", "comment"], default: "comment" }, tags: { type: "array", description: "Tags for categorizing the log entry", items: { type: "string" } } }, required: ["plan_id", "node_id", "content"] } }, { name: "get_logs", description: "Get log entries for a node", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID" }, log_type: { type: "string", description: "Filter by log type", enum: ["progress", "reasoning", "challenge", "decision", "comment"] }, limit: { type: "integer", description: "Maximum number of logs to return", default: 50 } }, required: ["plan_id", "node_id"] } }, // ===== ARTIFACT MANAGEMENT ===== { name: "manage_artifact", description: "Add, get, or search for artifacts", inputSchema: { type: "object", properties: { action: { type: "string", description: "Action to perform", enum: ["add", "get", "search", "list"] }, plan_id: { type: "string", description: "Plan ID" }, node_id: { type: "string", description: "Node ID" }, artifact_id: { type: "string", description: "Artifact ID (for 'get' action)" }, name: { type: "string", description: "Artifact name (for 'add' or 'search')" }, content_type: { type: "string", description: "Content MIME type (for 'add')" }, url: { type: "string", description: "URL where artifact can be accessed (for 'add')" }, metadata: { type: "object", description: "Additional metadata (for 'add')" } }, required: ["action", "plan_id", "node_id"] } }, // ===== BATCH OPERATIONS ===== { name: "batch_update_nodes", description: "Update multiple nodes at once", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, updates: { type: "array", description: "List of node updates", items: { type: "object", properties: { node_id: { type: "string", description: "Node ID" }, status: { type: "string", enum: ["not_started", "in_progress", "completed", "blocked"] }, title: { type: "string" }, description: { type: "string" } }, required: ["node_id"] } } }, required: ["plan_id", "updates"] } }, { name: "batch_get_artifacts", description: "Get multiple artifacts at once", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, artifact_requests: { type: "array", description: "List of artifact requests", items: { type: "object", properties: { node_id: { type: "string", description: "Node ID" }, artifact_id: { type: "string", description: "Artifact ID" } }, required: ["node_id", "artifact_id"] } } }, required: ["plan_id", "artifact_requests"] } }, // ===== PLAN STRUCTURE & SUMMARY ===== { name: "get_plan_structure", description: "Get the complete hierarchical structure of a plan", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" }, include_details: { type: "boolean", description: "Include full node details", default: false } }, required: ["plan_id"] } }, { name: "get_plan_summary", description: "Get a comprehensive summary with statistics", inputSchema: { type: "object", properties: { plan_id: { type: "string", description: "Plan ID" } }, required: ["plan_id"] } } ] }; }); // Handler for calling tools server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Only log in development mode if (process.env.NODE_ENV === 'development') { console.error(`Calling tool: ${name} with arguments:`, args); } try { // ===== UNIFIED SEARCH TOOL ===== if (name === "search") { const { scope, scope_id, query, filters = {} } = args; let results = []; switch (scope) { case "global": // Global search across all plans const searchWrapper = require('./tools/search-wrapper'); results = await searchWrapper.globalSearch(query); break; case "plans": // Search only in plan titles/descriptions const plans = await apiClient.plans.getPlans(); // Handle wildcard queries if (query === '*' || query === '' || !query) { // Return all plans (with optional status filter) results = plans.filter(plan => !filters.status || plan.status === filters.status ); } else { // Normal search const queryLower = query.toLowerCase(); results = plans.filter(plan => { const titleMatch = plan.title.toLowerCase().includes(queryLower); const descMatch = plan.description?.toLowerCase().includes(queryLower); const statusMatch = !filters.status || plan.status === filters.status; return (titleMatch || descMatch) && statusMatch; }); } break; case "plan": // Search within a specific plan if (!scope_id) { throw new Error("scope_id (plan_id) is required when scope is 'plan'"); } const searchWrapperPlan = require('./tools/search-wrapper'); results = await searchWrapperPlan.searchPlan(scope_id, query); break; case "node": // Search within a specific node's children if (!scope_id) { throw new Error("scope_id (node_id) is required when scope is 'node'"); } // This would need a specific implementation results = []; break; default: // Default to global search const searchWrapperDefault = require('./tools/search-wrapper'); results = await searchWrapperDefault.globalSearch(query); } // Apply filters if (filters.type) { results = results.filter(item => item.type === filters.type); } if (filters.limit) { results = results.slice(0, filters.limit); } return formatResponse({ query, scope, scope_id, filters, count: results.length, results }); } // ===== PLAN MANAGEMENT ===== if (name === "list_plans") { const { status } = args; const plans = await apiClient.plans.getPlans(); const filteredPlans = status ? plans.filter(p => p.status === status) : plans; return formatResponse(filteredPlans); } if (name === "create_plan") { const result = await apiClient.plans.createPlan(args); return formatResponse(result); } if (name === "update_plan") { const { plan_id, ...planData } = args; const result = await apiClient.plans.updatePlan(plan_id, planData); return formatResponse(result); } if (name === "delete_plan") { const { plan_id } = args; await apiClient.plans.deletePlan(plan_id); return formatResponse({ success: true, message: `Plan ${plan_id} deleted successfully` }); } // ===== NODE MANAGEMENT ===== if (name === "create_node") { const { plan_id, ...nodeData } = args; const result = await apiClient.nodes.createNode(plan_id, nodeData); return formatResponse(result); } if (name === "update_node") { const { plan_id, node_id, ...nodeData } = args; const result = await apiClient.nodes.updateNode(plan_id, node_id, nodeData); return formatResponse(result); } if (name === "delete_node") { const { plan_id, node_id } = args; await apiClient.nodes.deleteNode(plan_id, node_id); return formatResponse({ success: true, message: `Node ${node_id} and its children deleted successfully` }); } if (name === "move_node") { const { plan_id, node_id, parent_id, order_index } = args; try { // Call the move endpoint - using POST as per API definition const response = await apiClient.axiosInstance.post( `/plans/${plan_id}/nodes/${node_id}/move`, { parent_id: parent_id || null, order_index: order_index !== undefined ? order_index : null } ); return formatResponse(response.data); } catch (error) { // If endpoint still doesn't work, try updating the node directly if (error.response && error.response.status === 404) { console.error('Move endpoint not found, trying direct update'); // Fallback to updating the node's parent_id via regular update const updateResponse = await apiClient.nodes.updateNode(plan_id, node_id, { parent_id: parent_id || null, order_index: order_index !== undefined ? order_index : null }); return formatResponse(updateResponse); } throw error; } } if (name === "get_node_context") { const { plan_id, node_id } = args; // Get node with context const response = await apiClient.axiosInstance.get( `/plans/${plan_id}/nodes/${node_id}/context` ); return formatResponse(response.data); } if (name === "get_node_ancestry") { const { plan_id, node_id } = args; // Get node ancestry const response = await apiClient.axiosInstance.get( `/plans/${plan_id}/nodes/${node_id}/ancestry` ); return formatResponse(response.data); } // ===== LOGGING ===== if (name === "add_log") { const { plan_id, node_id, content, log_type = "comment", tags } = args; const logData = { content, log_type, tags }; const result = await apiClient.logs.addLogEntry(plan_id, node_id, logData); return formatResponse(result); } if (name === "get_logs") { const { plan_id, node_id, log_type, limit = 50 } = args; let logs = await apiClient.logs.getLogs(plan_id, node_id); // Apply filters if (log_type) { logs = logs.filter(log => log.log_type === log_type); } // Apply limit logs = logs.slice(0, limit); return formatResponse(logs); } // ===== ARTIFACT MANAGEMENT ===== if (name === "manage_artifact") { const { action, plan_id, node_id, ...params } = args; switch (action) { case "add": const { name, content_type, url, metadata } = params; const newArtifact = await apiClient.artifacts.addArtifact(plan_id, node_id, { name, content_type, url, metadata }); return formatResponse(newArtifact); case "get": const { artifact_id } = params; const artifact = await apiClient.artifacts.getArtifact(plan_id, node_id, artifact_id); const content = await apiClient.artifacts.getArtifactContent(plan_id, node_id, artifact_id); return formatResponse({ ...artifact, content }); case "search": const { name: searchName } = params; const artifacts = await apiClient.artifacts.getArtifacts(plan_id, node_id); const searchLower = searchName.toLowerCase(); const matches = artifacts.filter(a => a.name.toLowerCase().includes(searchLower) ); return formatResponse(matches); case "list": const allArtifacts = await apiClient.artifacts.getArtifacts(plan_id, node_id); return formatResponse(allArtifacts); default: throw new Error(`Unknown artifact action: ${action}`); } } // ===== BATCH OPERATIONS ===== if (name === "batch_update_nodes") { const { plan_id, updates } = args; const results = []; const errors = []; for (const update of updates) { const { node_id, ...updateData } = update; try { const result = await apiClient.nodes.updateNode(plan_id, node_id, updateData); results.push({ node_id, success: true, data: result }); } catch (error) { errors.push({ node_id, success: false, error: error.message }); } } return formatResponse({ total: updates.length, successful: results.length, failed: errors.length, results, errors }); } if (name === "batch_get_artifacts") { const { plan_id, artifact_requests } = args; const results = []; const errors = []; for (const request of artifact_requests) { const { node_id, artifact_id } = request; try { const artifact = await apiClient.artifacts.getArtifact(plan_id, node_id, artifact_id); const content = await apiClient.artifacts.getArtifactContent(plan_id, node_id, artifact_id); results.push({ node_id, artifact_id, success: true, data: { ...artifact, content } }); } catch (error) { errors.push({ node_id, artifact_id, success: false, error: error.message }); } } return formatResponse({ total: artifact_requests.length, successful: results.length, failed: errors.length, results, errors }); } // ===== PLAN STRUCTURE & SUMMARY ===== if (name === "get_plan_structure") { const { plan_id, include_details = false } = args; const plan = await apiClient.plans.getPlan(plan_id); const nodes = await apiClient.nodes.getNodes(plan_id); // The API already returns a tree structure, not a flat list // If it's already hierarchical, use it directly let structure; if (Array.isArray(nodes) && nodes.length > 0 && nodes[0].children !== undefined) { // Already hierarchical - use directly structure = nodes; } else { // Flat list - build hierarchy structure = buildNodeHierarchy(nodes, include_details); } return formatResponse({ plan: { id: plan.id, title: plan.title, status: plan.status, description: plan.description }, structure }); } if (name === "get_plan_summary") { const { plan_id } = args; const plan = await apiClient.plans.getPlan(plan_id); const nodes = await apiClient.nodes.getNodes(plan_id); // Calculate statistics const stats = calculatePlanStatistics(nodes); return formatResponse({ plan: { id: plan.id, title: plan.title, status: plan.status, description: plan.description, created_at: plan.created_at, updated_at: plan.updated_at }, statistics: stats, progress_percentage: stats.total > 0 ? ((stats.status_counts.completed / stats.total) * 100).toFixed(1) : 0 }); } // Tool not found throw new Error(`Unknown tool: ${name}`); } catch (error) { if (process.env.NODE_ENV === 'development') { console.error(`Error calling tool ${name}:`, error); } return { isError: true, content: [ { type: "text", text: `Error: ${error.message}` } ] }; } }); if (process.env.NODE_ENV === 'development') { console.error('Tools setup complete'); } } /** * Build hierarchical node structure */ function buildNodeHierarchy(nodes, includeDetails = false) { if (!nodes || nodes.length === 0) { return []; } // Debug logging to understand the structure if (process.env.NODE_ENV === 'development') { console.error('Building hierarchy for nodes:', nodes.length); if (nodes[0]) { console.error('Sample node:', { id: nodes[0].id, parent_id: nodes[0].parent_id, node_type: nodes[0].node_type }); } } const nodeMap = new Map(); const rootNodes = []; // First pass: create all nodes in the map nodes.forEach(node => { const nodeData = includeDetails ? { ...node } : { id: node.id, title: node.title, node_type: node.node_type, status: node.status, parent_id: node.parent_id, order_index: node.order_index }; // Initialize with empty children array nodeMap.set(node.id, { ...nodeData, children: [] }); }); // Second pass: build parent-child relationships nodes.forEach(node => { const currentNode = nodeMap.get(node.id); if (node.parent_id) { const parent = nodeMap.get(node.parent_id); if (parent) { // Add as child to parent parent.children.push(currentNode); } else { // Parent not found, treat as root if (process.env.NODE_ENV === 'development') { console.error(`Parent ${node.parent_id} not found for node ${node.id}`); } rootNodes.push(currentNode); } } else { // No parent_id means it's a root node rootNodes.push(currentNode); } }); // Special case: if we have a single root node of type 'root', return its children if (rootNodes.length === 1 && rootNodes[0].node_type === 'root') { // Return the root node itself with its children const rootNode = rootNodes[0]; // Sort children by order_index const sortNodes = (nodeArray) => { nodeArray.sort((a, b) => { const orderA = a.order_index ?? 999; const orderB = b.order_index ?? 999; return orderA - orderB; }); nodeArray.forEach(node => { if (node.children && node.children.length > 0) { sortNodes(node.children); } }); }; sortNodes(rootNode.children); return [rootNode]; // Return root with its properly sorted children } // Sort all root nodes and their children const sortNodes = (nodeArray) => { nodeArray.sort((a, b) => { const orderA = a.order_index ?? 999; const orderB = b.order_index ?? 999; return orderA - orderB; }); nodeArray.forEach(node => { if (node.children && node.children.length > 0) { sortNodes(node.children); } }); }; sortNodes(rootNodes); return rootNodes; } /** * Calculate plan statistics */ function calculatePlanStatistics(nodes) { const stats = { total: 0, type_counts: { root: 0, phase: 0, task: 0, milestone: 0 }, status_counts: { not_started: 0, in_progress: 0, completed: 0, blocked: 0 }, in_progress_nodes: [], blocked_nodes: [] }; const processNode = (node) => { stats.total++; if (node.node_type && stats.type_counts[node.node_type] !== undefined) { stats.type_counts[node.node_type]++; } if (node.status && stats.status_counts[node.status] !== undefined) { stats.status_counts[node.status]++; if (node.status === 'in_progress') { stats.in_progress_nodes.push({ id: node.id, title: node.title, type: node.node_type }); } else if (node.status === 'blocked') { stats.blocked_nodes.push({ id: node.id, title: node.title, type: node.node_type }); } } if (node.children && node.children.length > 0) { node.children.forEach(processNode); } }; nodes.forEach(processNode); return stats; } module.exports = { setupTools };

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TAgents/agent-planner-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server