search
Search plans, nodes, and content within a planning system to find specific information using queries and filters.
Instructions
Universal search tool for plans, nodes, and content
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| scope | No | Search scope | global |
| scope_id | No | Plan ID (if scope is 'plan') or Node ID (if scope is 'node') | |
| query | Yes | Search query | |
| filters | No | Optional filters |
Implementation Reference
- src/tools.js:443-517 (handler)Handler for the 'search' MCP tool. Dispatches based on scope to globalSearch or searchPlan wrappers from search-wrapper.js, or client-side filtering for plans. Applies additional filters and formats response using formatResponse.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 }); }
- src/tools.js:61-104 (schema)Schema definition for the 'search' tool, defining input parameters: scope (global/plans/plan/node), scope_id, query (required), and optional filters (status, type, limit).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"] } },
- src/index.js:4-54 (registration)Registers the MCP tools on the server instance by calling setupTools(server), which includes the 'search' tool.const { setupTools } = require('./tools'); require('dotenv').config(); /** * Initialize the Planning System MCP Server * * Features: * - Simplified architecture with tools-only interface * - Full CRUD operations on all entities * - Unified search across all scopes * - Batch operations for efficiency * - Structured JSON responses * - Comprehensive logging system */ async function main() { const isDev = process.env.NODE_ENV === 'development'; if (isDev) { console.error('Initializing Planning System MCP Server...'); } try { // Log environment settings console.error(`API URL: ${process.env.API_URL || 'http://localhost:3000'}`); // Check for token const userApiToken = process.env.USER_API_TOKEN || process.env.API_TOKEN; console.error(`User API Token: ${userApiToken ? '***' + userApiToken.slice(-4) : 'NOT SET'}`); console.error(`MCP Server Name: ${process.env.MCP_SERVER_NAME || 'planning-system-mcp'}`); console.error(`MCP Server Version: ${process.env.MCP_SERVER_VERSION || '0.2.0'}`); // Validate required environment variables if (!userApiToken) { throw new Error('USER_API_TOKEN environment variable is required. Please generate one from the Agent Planner UI and set it in .env file.'); } // Create MCP server instance const server = new Server({ name: process.env.MCP_SERVER_NAME || "planning-system-mcp", version: process.env.MCP_SERVER_VERSION || "0.2.0" }, { capabilities: { tools: {} } }); console.error('MCP Server created'); // Setup tools setupTools(server);
- src/tools/search-wrapper.js:17-71 (helper)Helper function searchPlan that wraps apiClient.search.searchPlan, normalizes response to return results array directly.async function searchPlan(planId, query) { try { // Call the original search function from the API client const response = await apiClient.search.searchPlan(planId, query); // Log the actual response for debugging if (process.env.NODE_ENV === 'development') { console.log('Search plan response:', typeof response, Object.keys(response || {})); } // Handle different response formats if (!response) { return []; } // If response is already an array, return it if (Array.isArray(response)) { return response; } // If response has results array if (response.results && Array.isArray(response.results)) { return response.results; } // If response has other properties that are arrays const results = []; Object.keys(response).forEach(key => { if (Array.isArray(response[key])) { response[key].forEach(item => { results.push({ ...item, type: item.type || key, source: 'plan_search' }); }); } }); if (results.length > 0) { return results; } // Try to parse response as a search result object if (response.query !== undefined && response.count !== undefined) { return response.results || []; } console.error('Unexpected search response format:', JSON.stringify(response).substring(0, 200)); return []; } catch (error) { console.error('Error in searchPlan wrapper:', error.message); return []; } }
- src/tools/search-wrapper.js:79-182 (helper)Helper function globalSearch that wraps apiClient.search.globalSearch, handles various response formats, flattens and enriches results with type, category, source.async function globalSearch(query) { try { // Call the original global search function const response = await apiClient.search.globalSearch(query); if (process.env.NODE_ENV === 'development') { console.log('Global search response type:', typeof response); if (response) { console.log('Response keys:', Object.keys(response)); } } // Handle different response formats if (!response) { return []; } // If response is already an array of results if (Array.isArray(response)) { return response.map(item => ({ ...item, type: item.type || 'unknown', source: 'global_search' })); } // If response has a results property if (response.results !== undefined) { // If results is already an array, return it if (Array.isArray(response.results)) { return response.results.map(item => ({ ...item, type: item.type || 'unknown', source: 'global_search' })); } // If results is an object with categories if (typeof response.results === 'object') { const allResults = []; // Collect results from each category (plans, nodes, comments, etc.) Object.keys(response.results).forEach(category => { if (Array.isArray(response.results[category])) { // Add category type to each result const categoryResults = response.results[category].map(item => ({ ...item, type: item.type || category, category, source: 'global_search' })); allResults.push(...categoryResults); } }); return allResults; } } // Check if response has categorized results (plans, nodes, etc.) const categories = ['plans', 'nodes', 'comments', 'logs', 'artifacts']; const allResults = []; categories.forEach(category => { if (response[category] && Array.isArray(response[category])) { response[category].forEach(item => { allResults.push({ ...item, type: item.type || category.slice(0, -1), // Remove 's' from category category, source: 'global_search' }); }); } }); if (allResults.length > 0) { return allResults; } // Generic handler for any object with arrays Object.keys(response).forEach(key => { if (Array.isArray(response[key]) && key !== 'results') { response[key].forEach(item => { allResults.push({ ...item, type: item.type || key, category: key, source: 'global_search' }); }); } }); return allResults; } catch (error) { console.error('Error in globalSearch wrapper:', error.message); if (error.response) { console.error('API error status:', error.response.status); console.error('API error data:', error.response.data); } return []; } }