search_portfolio
Search your local portfolio elements by name, keywords, tags, or descriptions to quickly find personas, skills, templates, agents, memories, or ensembles using metadata-based lookups.
Instructions
Search your local portfolio by content name, metadata, keywords, tags, or description. This searches your local elements using the portfolio index for fast metadata-based lookups.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search query. Can match element names, keywords, tags, triggers, or descriptions. Examples: 'creative writer', 'debug', 'code review', 'research'. | |
| type | No | Limit search to specific element type. If not specified, searches all types. | |
| fuzzy_match | No | Enable fuzzy matching for approximate name matches. Defaults to true. | |
| max_results | No | Maximum number of results to return. Defaults to 20. | |
| include_keywords | No | Include keyword matching in search. Defaults to true. | |
| include_tags | No | Include tag matching in search. Defaults to true. | |
| include_triggers | No | Include trigger word matching in search (for personas). Defaults to true. | |
| include_descriptions | No | Include description text matching in search. Defaults to true. |
Implementation Reference
- src/server/tools/PortfolioTools.ts:178-231 (registration)Defines and registers the 'search_portfolio' MCP tool, including full input schema, description, and handler that delegates to the server's searchPortfolio method.tool: { name: "search_portfolio", description: "Search your local portfolio by content name, metadata, keywords, tags, or description. This searches your local elements using the portfolio index for fast metadata-based lookups.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query. Can match element names, keywords, tags, triggers, or descriptions. Examples: 'creative writer', 'debug', 'code review', 'research'.", }, type: { type: "string", enum: ["personas", "skills", "templates", "agents", "memories", "ensembles"], description: "Limit search to specific element type. If not specified, searches all types.", }, fuzzy_match: { type: "boolean", description: "Enable fuzzy matching for approximate name matches. Defaults to true.", }, max_results: { type: "number", description: "Maximum number of results to return. Defaults to 20.", }, include_keywords: { type: "boolean", description: "Include keyword matching in search. Defaults to true.", }, include_tags: { type: "boolean", description: "Include tag matching in search. Defaults to true.", }, include_triggers: { type: "boolean", description: "Include trigger word matching in search (for personas). Defaults to true.", }, include_descriptions: { type: "boolean", description: "Include description text matching in search. Defaults to true.", }, }, required: ["query"], }, }, handler: (args: SearchPortfolioArgs) => server.searchPortfolio({ query: args.query, elementType: args.type as any, fuzzyMatch: args.fuzzy_match, maxResults: args.max_results, includeKeywords: args.include_keywords, includeTags: args.include_tags, includeTriggers: args.include_triggers, includeDescriptions: args.include_descriptions }) },
- JSON Schema for search_portfolio tool input validation, defining all parameters like query (required), type filter, fuzzy matching, limits, and include flags.inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query. Can match element names, keywords, tags, triggers, or descriptions. Examples: 'creative writer', 'debug', 'code review', 'research'.", }, type: { type: "string", enum: ["personas", "skills", "templates", "agents", "memories", "ensembles"], description: "Limit search to specific element type. If not specified, searches all types.", }, fuzzy_match: { type: "boolean", description: "Enable fuzzy matching for approximate name matches. Defaults to true.", }, max_results: { type: "number", description: "Maximum number of results to return. Defaults to 20.", }, include_keywords: { type: "boolean", description: "Include keyword matching in search. Defaults to true.", }, include_tags: { type: "boolean", description: "Include tag matching in search. Defaults to true.", }, include_triggers: { type: "boolean", description: "Include trigger word matching in search (for personas). Defaults to true.", }, include_descriptions: { type: "boolean", description: "Include description text matching in search. Defaults to true.", }, }, required: ["query"], },
- Core search handler that implements portfolio search across multiple fields (name, filename, keywords, tags, triggers, descriptions) using tokenized query matching, scoring, deduplication, and optional filters. This is the primary execution logic for search_portfolio.public async search(query: string, options: SearchOptions = {}): Promise<SearchResult[]> { const index = await this.getIndex(); // Normalize query for security const normalizedQuery = UnicodeValidator.normalize(query); if (!normalizedQuery.isValid) { logger.warn('Invalid Unicode in search query', { issues: normalizedQuery.detectedIssues }); return []; } const safeQuery = normalizedQuery.normalizedContent.toLowerCase().trim(); const queryTokens = safeQuery.split(/\s+/).filter(token => token.length > 0); if (queryTokens.length === 0) { return []; } const results: SearchResult[] = []; const seenPaths = new Set<string>(); const maxResults = options.maxResults || 20; // Helper to add unique results const addResult = (entry: IndexEntry, matchType: SearchResult['matchType'], score: number = 1) => { if (!seenPaths.has(entry.filePath) && results.length < maxResults) { // Filter by element type if specified if (options.elementType && entry.elementType !== options.elementType) { return; } seenPaths.add(entry.filePath); results.push({ entry, matchType, score }); } }; // 1. Search by name (highest priority) for (const [name, entry] of index.byName) { if (this.matchesQuery(name, queryTokens)) { addResult(entry, 'name', 3); } } // 2. Search by filename for (const [filename, entry] of index.byFilename) { if (this.matchesQuery(filename, queryTokens)) { addResult(entry, 'filename', 2.5); } } // 3. Search by keywords if (options.includeKeywords !== false) { for (const [keyword, entries] of index.byKeyword) { if (this.matchesQuery(keyword, queryTokens)) { for (const entry of entries) { addResult(entry, 'keyword', 2); } } } } // 4. Search by tags if (options.includeTags !== false) { for (const [tag, entries] of index.byTag) { if (this.matchesQuery(tag, queryTokens)) { for (const entry of entries) { addResult(entry, 'tag', 2); } } } } // 5. Search by triggers if (options.includeTriggers !== false) { for (const [trigger, entries] of index.byTrigger) { if (this.matchesQuery(trigger, queryTokens)) { for (const entry of entries) { addResult(entry, 'trigger', 1.8); } } } } // 6. Search by description if (options.includeDescriptions !== false) { for (const [_, entry] of index.byName) { if (entry.metadata.description && this.matchesQuery(entry.metadata.description.toLowerCase(), queryTokens)) { addResult(entry, 'description', 1.5); } } } // Sort by score (descending) results.sort((a, b) => b.score - a.score); logger.debug('Portfolio search completed', { query: safeQuery, resultCount: results.length, totalIndexed: index.byName.size }); return results; }
- src/server/types.ts:71-71 (helper)TypeScript interface definition for the searchPortfolio method in IToolHandler, specifying exact parameter types and structure used by the MCP tool server.searchPortfolio(options: {query: string; elementType?: string; fuzzyMatch?: boolean; maxResults?: number; includeKeywords?: boolean; includeTags?: boolean; includeTriggers?: boolean; includeDescriptions?: boolean}): Promise<any>;
- Supporting findByName method for exact, filename, and fuzzy name-based lookups, used internally by search and other portfolio operations.public async findByName(name: string, options: SearchOptions = {}): Promise<IndexEntry | null> { const index = await this.getIndex(); // Normalize input for security const normalizedName = UnicodeValidator.normalize(name); if (!normalizedName.isValid) { logger.warn('Invalid Unicode in search name', { issues: normalizedName.detectedIssues }); return null; } const safeName = normalizedName.normalizedContent; // Try exact match first (case insensitive) const exactMatch = index.byName.get(safeName.toLowerCase()); if (exactMatch) { logger.debug('Found exact name match', { name: safeName, filePath: exactMatch.filePath }); return exactMatch; } // Try filename match const filenameMatch = index.byFilename.get(safeName.toLowerCase()); if (filenameMatch) { logger.debug('Found filename match', { name: safeName, filePath: filenameMatch.filePath }); return filenameMatch; } // Try fuzzy matching if enabled if (options.fuzzyMatch !== false) { const fuzzyMatch = this.findFuzzyMatch(safeName, index, options); if (fuzzyMatch) { logger.debug('Found fuzzy match', { name: safeName, matchName: fuzzyMatch.metadata.name, filePath: fuzzyMatch.filePath }); return fuzzyMatch; } } logger.debug('No match found for name', { name: safeName }); return null; }