Skip to main content
Glama

search_portfolio

Search local portfolio elements by name, keywords, tags, or descriptions to quickly find personas, skills, templates, and other assets 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
NameRequiredDescriptionDefault
queryYesSearch query. Can match element names, keywords, tags, triggers, or descriptions. Examples: 'creative writer', 'debug', 'code review', 'research'.
typeNoLimit search to specific element type. If not specified, searches all types.
fuzzy_matchNoEnable fuzzy matching for approximate name matches. Defaults to true.
max_resultsNoMaximum number of results to return. Defaults to 20.
include_keywordsNoInclude keyword matching in search. Defaults to true.
include_tagsNoInclude tag matching in search. Defaults to true.
include_triggersNoInclude trigger word matching in search (for personas). Defaults to true.
include_descriptionsNoInclude description text matching in search. Defaults to true.

Implementation Reference

  • Registers the 'search_portfolio' MCP tool with full input schema definition and handler that delegates to the internal server.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
      })
    },
  • TypeScript interface definition for the searchPortfolio method signature used by the tool handler.
      searchPortfolio(options: {query: string; elementType?: string; fuzzyMatch?: boolean; maxResults?: number; includeKeywords?: boolean; includeTags?: boolean; includeTriggers?: boolean; includeDescriptions?: boolean}): Promise<any>;
      searchAll(options: {query: string; sources?: string[]; elementType?: string; page?: number; pageSize?: number; sortBy?: string}): Promise<any>;
      
      // New unified config and sync handlers
      handleConfigOperation(options: any): Promise<any>;
      handleSyncOperation(options: any): Promise<any>;
    
      // Enhanced Index tools
      findSimilarElements(options: {elementName: string; elementType?: string; limit: number; threshold: number}): Promise<any>;
      getElementRelationships(options: {elementName: string; elementType?: string; relationshipTypes?: string[]}): Promise<any>;
      searchByVerb(options: {verb: string; limit: number}): Promise<any>;
      getRelationshipStats(): Promise<any>;
    }
  • Core search implementation in PortfolioIndexManager.search() that performs multi-field search over indexed portfolio elements, used by the tool handler.
    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;
    }
  • findByName helper for exact and fuzzy matching of element names by metadata or filename, supporting the search functionality.
    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;
    }

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/DollhouseMCP/DollhouseMCP'

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