Skip to main content
Glama

Search Knowledge

search_knowledge

Quickly locate technical details and related content across project documents using intelligent keyword search with result grouping. Ideal for researching before creating new content or verifying existing documentation.

Instructions

Search project knowledge documents for keywords with intelligent result grouping.

When to use this tool:

  • Finding information across multiple documents

  • Locating specific technical details

  • Discovering related content

  • Checking if topic is already documented

  • Researching before creating new content

Key features:

  • Case-insensitive full-text search

  • Searches document body, titles, and content

  • Groups results by document

  • Returns matching chapters with context

  • Space-separated keyword support

You should:

  1. Use specific keywords for better results

  2. Try multiple search terms if needed

  3. Search before creating new documents

  4. Use 2-3 word phrases for precision

  5. Review all results before concluding

  6. Consider variations of technical terms

  7. Check both titles and content matches

DO NOT use when:

  • Know exact document and chapter

  • Need complete document listing

  • Searching for project main content

Returns: {success: bool, total_documents: int, total_matches: int, results: [...], error?: str}

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idYesThe project identifier
queryYesSpace-separated keywords to search for

Implementation Reference

  • Primary handler function for the search_knowledge tool. Handles input validation, project lookup, keyword parsing, delegates to searchDocumentsAsync, transforms results to match expected output format (total_documents, total_matches, results with document and chapters), and formats the MCP response with logging.
    async searchKnowledgeAsync(params: {
      project_id: z.infer<typeof secureProjectIdSchema>;
      query: z.infer<typeof secureSearchQuerySchema>;
    }): Promise<string> {
      const context = this.createContext('search_knowledge', params);
    
      try {
        const { project_id, query } = params;
        const projectInfo = await getProjectDirectoryAsync(this.storagePath, project_id);
    
        // Project doesn't exist - return empty results without creating ghost entry
        if (!projectInfo) {
          this.logSuccess('search_knowledge', { project_id }, context);
          return this.formatSuccessResponse({
            total_documents: 0,
            total_matches: 0,
            results: [],
          });
        }
    
        const [, projectPath] = projectInfo;
    
        // Parse search keywords
        const keywords = query
          .toLowerCase()
          .split(/\s+/)
          .filter((k) => k.length > 0);
    
        // Search documents - pass projectPath, not knowledgePath
        const results = await searchDocumentsAsync(projectPath, keywords.join(' '));
    
        // Transform results for response - matching test expectations
        const transformedResults = results.map((result: SearchResult) => ({
          document: result.file,
          chapters: result.matching_chapters.map((ch) => ({
            title: ch.chapter || '(Document Introduction)',
            matches: ch.keywords_found.length,
          })),
        }));
    
        this.logSuccess('search_knowledge', { project_id }, context);
        return this.formatSuccessResponse({
          total_documents: transformedResults.length,
          total_matches: results.reduce((sum, r) => sum + r.match_count, 0),
          results: transformedResults,
        });
      } catch (error) {
        const mcpError =
          error instanceof MCPError
            ? error
            : new MCPError(
                MCPErrorCode.INTERNAL_ERROR,
                `Search failed: ${error instanceof Error ? error.message : String(error)}`,
                {
                  project_id: params.project_id,
                  query: params.query,
                  traceId: context.traceId,
                }
              );
        this.logError(
          'search_knowledge',
          {
            project_id: params.project_id,
            query: params.query,
          },
          mcpError,
          context
        );
        return this.formatErrorResponse(mcpError, context);
      }
    }
  • Core search helper function called by the handler. Performs full-text keyword search across all knowledge documents in a project, parsing MD files, extracting chapters, finding matches with context, and grouping results by document and chapter.
    export async function searchDocumentsAsync(
      projectPath: string,
      query: string
    ): Promise<SearchResult[]> {
      if (!query) {
        return [];
      }
    
      const knowledgeDir = join(projectPath, 'knowledge');
      try {
        await access(knowledgeDir);
      } catch {
        return [];
      }
    
      // Split query into individual keywords
      const keywords = query
        .split(/\s+/)
        .map((kw) => kw.trim().toLowerCase())
        .filter((kw) => kw);
    
      if (keywords.length === 0) {
        return [];
      }
    
      // Structure to aggregate results by document
      const documentResults: Record<
        string,
        {
          metadata: DocumentMetadata;
          matching_chapters: Record<
            string,
            {
              chapter: string;
              keywords_found: Set<string>;
              match_context: Record<string, string[]>;
              chapter_summary: string;
            }
          >;
        }
      > = {};
    
      // Search through all markdown files
      const files = await readdir(knowledgeDir);
      const mdFiles = files.filter((f) => f.endsWith('.md'));
    
      for (const mdFile of mdFiles) {
        try {
          const filePath = join(knowledgeDir, mdFile);
          const content = await readFile(filePath, 'utf8');
    
          // Parse document
          const [metadata, body] = parseDocument(content);
          const chapters = parseChapters(body);
    
          // Search each keyword
          for (const keyword of keywords) {
            const bodyLower = body.toLowerCase();
    
            // If no chapters, search entire body
            if (chapters.length === 0 && bodyLower.includes(keyword)) {
              const contexts: string[] = [];
              let startPos = 0;
    
              let index = bodyLower.indexOf(keyword, startPos);
              while (index !== -1) {
                // Extract context
                const ctxStart = Math.max(0, index - 50);
                const ctxEnd = Math.min(body.length, index + keyword.length + 50);
                let context = body.slice(ctxStart, ctxEnd).trim();
    
                if (ctxStart > 0) context = '...' + context;
                if (ctxEnd < body.length) context = context + '...';
    
                contexts.push(context);
                startPos = index + 1;
                index = bodyLower.indexOf(keyword, startPos);
              }
    
              if (contexts.length > 0) {
                // Initialize document entry if needed
                if (!(mdFile in documentResults)) {
                  documentResults[mdFile] = {
                    metadata,
                    matching_chapters: {},
                  };
                }
    
                // Add body-level match as a special chapter
                if (!('_document_body' in documentResults[mdFile].matching_chapters)) {
                  documentResults[mdFile].matching_chapters['_document_body'] = {
                    chapter: '',
                    keywords_found: new Set(),
                    match_context: {},
                    chapter_summary: '',
                  };
                }
    
                const chapterData = documentResults[mdFile].matching_chapters['_document_body'];
                chapterData.keywords_found.add(keyword);
                if (!(keyword in chapterData.match_context)) {
                  chapterData.match_context[keyword] = [];
                }
                chapterData.match_context[keyword].push(...contexts);
              }
            }
    
            // Search in chapters
            for (const chapter of chapters) {
              if (chapter.content.toLowerCase().includes(keyword)) {
                const contexts: string[] = [];
                const chapterLower = chapter.content.toLowerCase();
                let startPos = 0;
    
                let index = chapterLower.indexOf(keyword, startPos);
                while (index !== -1) {
                  // Extract context
                  const ctxStart = Math.max(0, index - 50);
                  const ctxEnd = Math.min(chapter.content.length, index + keyword.length + 50);
                  let context = chapter.content.slice(ctxStart, ctxEnd).trim();
    
                  if (ctxStart > 0) context = '...' + context;
                  if (ctxEnd < chapter.content.length) context = context + '...';
    
                  contexts.push(context);
                  startPos = index + 1;
                  index = chapterLower.indexOf(keyword, startPos);
                }
    
                if (contexts.length > 0) {
                  // Initialize document entry if needed
                  if (!(mdFile in documentResults)) {
                    documentResults[mdFile] = {
                      metadata,
                      matching_chapters: {},
                    };
                  }
    
                  // Initialize chapter entry if needed
                  if (!(chapter.title in documentResults[mdFile].matching_chapters)) {
                    documentResults[mdFile].matching_chapters[chapter.title] = {
                      chapter: chapter.title,
                      keywords_found: new Set(),
                      match_context: {},
                      chapter_summary: chapter.summary,
                    };
                  }
    
                  const chapterData = documentResults[mdFile].matching_chapters[chapter.title];
                  chapterData.keywords_found.add(keyword);
                  if (!(keyword in chapterData.match_context)) {
                    chapterData.match_context[keyword] = [];
                  }
                  chapterData.match_context[keyword].push(...contexts);
                }
              }
            }
    
            // If chapters exist, also search for pre-chapter content
            if (chapters.length > 0 && bodyLower.includes(keyword)) {
              // Get the content before the first chapter
              const firstChapterStart = body.indexOf('\n##');
              if (firstChapterStart > 0) {
                const preChapterContent = body.slice(0, firstChapterStart);
                if (preChapterContent.toLowerCase().includes(keyword)) {
                  const contexts: string[] = [];
                  const contentLower = preChapterContent.toLowerCase();
                  let startPos = 0;
    
                  let index = contentLower.indexOf(keyword, startPos);
                  while (index !== -1) {
                    // Extract context
                    const ctxStart = Math.max(0, index - 50);
                    const ctxEnd = Math.min(preChapterContent.length, index + keyword.length + 50);
                    let context = preChapterContent.slice(ctxStart, ctxEnd).trim();
    
                    if (ctxStart > 0) context = '...' + context;
                    if (ctxEnd < preChapterContent.length) context = context + '...';
    
                    contexts.push(context);
                    startPos = index + 1;
                    index = contentLower.indexOf(keyword, startPos);
                  }
    
                  if (contexts.length > 0) {
                    // Initialize document entry if needed
                    if (!(mdFile in documentResults)) {
                      documentResults[mdFile] = {
                        metadata,
                        matching_chapters: {},
                      };
                    }
    
                    // Add pre-chapter content as a special chapter
                    if (!('_pre_chapter' in documentResults[mdFile].matching_chapters)) {
                      documentResults[mdFile].matching_chapters['_pre_chapter'] = {
                        chapter: '',
                        keywords_found: new Set(),
                        match_context: {},
                        chapter_summary: '',
                      };
                    }
    
                    const chapterData = documentResults[mdFile].matching_chapters['_pre_chapter'];
                    chapterData.keywords_found.add(keyword);
                    if (!(keyword in chapterData.match_context)) {
                      chapterData.match_context[keyword] = [];
                    }
                    chapterData.match_context[keyword].push(...contexts);
                  }
                }
              }
            }
          }
        } catch {
          // Skip files that can't be read
          continue;
        }
      }
    
      // Convert to document-centric format
      const results: SearchResult[] = [];
    
      for (const [filename, docData] of Object.entries(documentResults)) {
        // Convert matching chapters to array
        const matchingChapters: MatchingChapter[] = [];
    
        for (const [, chapterData] of Object.entries(docData.matching_chapters)) {
          matchingChapters.push({
            chapter: chapterData.chapter,
            keywords_found: Array.from(chapterData.keywords_found).sort(),
            match_context: chapterData.match_context,
            chapter_summary: chapterData.chapter_summary,
          });
        }
    
        results.push({
          file: filename,
          match_count: matchingChapters.length,
          metadata: docData.metadata,
          matching_chapters: matchingChapters,
        });
      }
    
      return results;
    }
  • MCP tool registration for 'search_knowledge', defining title, description, input schema (project_id, query), and handler that invokes SearchToolHandler.searchKnowledgeAsync and wraps response in MCP content format.
    server.registerTool(
      'search_knowledge',
      {
        title: 'Search Knowledge',
        description: TOOL_DESCRIPTIONS.search_knowledge,
        inputSchema: {
          project_id: secureProjectIdSchema.describe('The project identifier'),
          query: secureSearchQuerySchema.describe('Space-separated keywords to search for'),
        },
      },
      async ({ project_id, query }) => {
        const result = await searchHandler.searchKnowledgeAsync({ project_id, query });
        return {
          content: [
            {
              type: 'text',
              text: result,
            },
          ],
        };
      }
    );
  • Detailed tool description and usage instructions for search_knowledge, including when to use, key features, best practices, return format, used in registration.
      search_knowledge: `Search project knowledge documents for keywords with intelligent result grouping.
    
    When to use this tool:
    - Finding information across multiple documents
    - Locating specific technical details
    - Discovering related content
    - Checking if topic is already documented
    - Researching before creating new content
    
    Key features:
    - Case-insensitive full-text search
    - Searches document body, titles, and content
    - Groups results by document
    - Returns matching chapters with context
    - Space-separated keyword support
    
    You should:
    1. Use specific keywords for better results
    2. Try multiple search terms if needed
    3. Search before creating new documents
    4. Use 2-3 word phrases for precision
    5. Review all results before concluding
    6. Consider variations of technical terms
    7. Check both titles and content matches
    
    DO NOT use when:
    - Know exact document and chapter
    - Need complete document listing
    - Searching for project main content
    
    Returns: {success: bool, total_documents: int, total_matches: int, results: [...], error?: str}`,
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes key features like case-insensitive full-text search, scope (document body, titles, content), result grouping by document, and return format details. It also provides usage recommendations (e.g., use specific keywords, try multiple terms). However, it lacks explicit mention of potential limitations like rate limits or authentication needs, which would be helpful for a search tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections (purpose, when to use, key features, usage advice, exclusions, returns). While comprehensive, it could be more front-loaded by placing the return format earlier. Some sentences in the 'You should' list are slightly redundant (e.g., points about using specific keywords and 2-3 word phrases overlap), but overall it's efficient and informative.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (search functionality with 2 parameters), no annotations, and no output schema, the description does a good job of covering behavior, usage, and return format. It explains what the tool does, when to use it, and what to expect in the response. However, it could improve by explicitly mentioning error handling or performance considerations, which would make it more complete for an agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, providing clear documentation for both parameters ('project_id' and 'query'). The description adds some context by mentioning 'space-separated keyword support' and advising on keyword strategies, but it doesn't significantly enhance the parameter semantics beyond what the schema already defines. This meets the baseline of 3 for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose as 'Search project knowledge documents for keywords with intelligent result grouping', which is a specific verb+resource+scope combination. It distinguishes itself from sibling tools like 'get_knowledge_file' (retrieves a specific file) and 'list_chapters' (lists all chapters without search) by emphasizing search functionality across multiple documents with grouping.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit 'When to use this tool' scenarios (e.g., finding information across documents, locating technical details) and 'DO NOT use when' exclusions (e.g., when you know the exact document/chapter, need complete document listing). It also includes a 'You should' section with practical advice, offering clear guidance on when to use this tool versus alternatives like 'get_knowledge_file' for known documents.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

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/sven-borkert/knowledge-mcp'

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