Skip to main content
Glama
Hint-Services

Obsidian GitHub MCP

searchFiles

Search for notes, documents, and files within your Obsidian vault hosted on GitHub. Find specific content using GitHub search syntax, targeting filenames, paths, or file contents for precise results.

Instructions

Search for notes, documents, and files within your Obsidian vault on GitHub (my-organization/obsidian-vault). Find specific knowledge base content using GitHub's powerful search syntax. Supports searching in filenames, paths, and content.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pageNoPage number to retrieve (0-indexed)
perPageNoNumber of results per page
queryYesSearch query - can be a simple term or use GitHub search qualifiers
searchInNoWhere to search: 'filename' (exact filename match), 'path' (anywhere in file path), 'content' (file contents), or 'all' (comprehensive search)all

Implementation Reference

  • Main handler function that executes the searchFiles tool. Constructs GitHub code search queries tailored to searchIn (filename, path, content, all), fetches results via Octokit, formats output with match indicators, and provides detailed diagnostics/formatted response if no results.
    async ({ query, searchIn = "all", page = 0, perPage = 100 }) => {
      // Empty query is allowed - useful for listing files
      const repoQualifier = `repo:${this.config.owner}/${this.config.repo}`;
    
      // Build search query based on searchIn parameter
      let qualifiedQuery: string;
    
      if (searchIn === "filename") {
        // Search for exact filename matches
        qualifiedQuery = `filename:${
          query.includes(" ") ? `"${query}"` : query
        } ${repoQualifier}`;
      } else if (searchIn === "path") {
        // Search anywhere in the file path. The `in:path` qualifier searches for the
        // query term within the file path.
        qualifiedQuery = `${query} in:path ${repoQualifier}`;
      } else if (searchIn === "content") {
        // Search only in file contents. This is the default behavior without qualifiers.
        qualifiedQuery = `${query} ${repoQualifier}`;
      } else {
        // "all" - comprehensive search. The GitHub search API (legacy) does not
        // support OR operators. The best we can do in a single query is to search
        // in file content and file path. The `in:file,path` qualifier does this.
        // This will match the query term if it appears in the content or anywhere
        // in the full path of a file, which includes the filename.
        qualifiedQuery = `${query} in:file,path ${repoQualifier}`;
      }
    
      let searchResults: {
        items: Array<{ name: string; path: string }>;
        total_count: number;
      };
      try {
        searchResults = await this.handleRequest(async () => {
          return this.octokit.search.code({
            q: qualifiedQuery,
            page,
            per_page: perPage,
          });
        });
      } catch (error) {
        // Enhanced error messages with specific GitHub search issues
        const errorMessage =
          error instanceof Error ? error.message : String(error);
        if (errorMessage.includes("validation failed")) {
          throw new Error(
            `GitHub search query invalid: "${qualifiedQuery}". Try simpler terms or check syntax.`
          );
        }
        if (errorMessage.includes("rate limit")) {
          throw new Error(
            "GitHub code search rate limit exceeded. Wait a moment and try again."
          );
        }
        if (
          errorMessage.includes("Forbidden") ||
          errorMessage.includes("401")
        ) {
          throw new Error(
            "GitHub API access denied. Check that your token has 'repo' scope for private repositories."
          );
        }
        throw error; // Re-throw other errors
      }
    
      // Enhanced formatting with file sizes and relevance indicators
      const formattedResults = searchResults.items
        .map((item) => {
          const fileName = item.name;
          const filePath = item.path;
          // const score = item.score || 0; // Could be used for relevance ranking in future
    
          // Determine why this file matched
          let matchReason = "";
          if (searchIn === "filename") {
            matchReason = "📝 filename match";
          } else if (searchIn === "path") {
            matchReason = "📁 path match";
          } else if (searchIn === "content") {
            matchReason = "📄 content match";
          } else {
            // searchIn is 'all', so we deduce the reason
            if (fileName.toLowerCase().includes(query.toLowerCase())) {
              matchReason = "📝 filename match";
            } else if (filePath.toLowerCase().includes(query.toLowerCase())) {
              matchReason = "📁 path match";
            } else {
              matchReason = "📄 content match";
            }
          }
    
          return `- **${fileName}** (${filePath}) ${matchReason}`;
        })
        .join("\n");
    
      let resultText = `Found ${searchResults.total_count} files`;
      if (searchIn !== "all") {
        resultText += ` searching in ${searchIn}`;
      }
      resultText += `:\n\n${formattedResults}`;
    
      // If no results, run diagnostics and provide enhanced response
      if (searchResults.total_count === 0) {
        const diagnostics = await this.runSearchDiagnostics(query);
        return this.formatNoResultsResponse(
          searchResults,
          diagnostics,
          query,
          searchIn
        );
      }
    
      return {
        content: [
          {
            type: "text" as const,
            text: resultText,
          },
        ],
      };
    }
  • Zod input schema defining parameters for the searchFiles tool: query (required string), searchIn (enum: filename|path|content|all, default all), page (number, default 0), perPage (number, default 100).
    {
      query: z
        .string()
        .describe(
          "Search query - can be a simple term or use GitHub search qualifiers"
        ),
      searchIn: z
        .enum(["filename", "path", "content", "all"])
        .optional()
        .default("all")
        .describe(
          "Where to search: 'filename' (exact filename match), 'path' (anywhere in file path), 'content' (file contents), or 'all' (comprehensive search)"
        ),
      page: z
        .number()
        .optional()
        .default(0)
        .describe("Page number to retrieve (0-indexed)"),
      perPage: z
        .number()
        .optional()
        .default(100)
        .describe("Number of results per page"),
    },
  • Registration of the searchFiles tool via server.tool() within the registerGithubTools method of GithubClient.
    server.tool(
      "searchFiles",
      `Search for notes, documents, and files within your Obsidian vault on GitHub (${this.config.owner}/${this.config.repo}). Find specific knowledge base content using GitHub's powerful search syntax. Supports searching in filenames, paths, and content.`,
      {
        query: z
          .string()
          .describe(
            "Search query - can be a simple term or use GitHub search qualifiers"
          ),
        searchIn: z
          .enum(["filename", "path", "content", "all"])
          .optional()
          .default("all")
          .describe(
            "Where to search: 'filename' (exact filename match), 'path' (anywhere in file path), 'content' (file contents), or 'all' (comprehensive search)"
          ),
        page: z
          .number()
          .optional()
          .default(0)
          .describe("Page number to retrieve (0-indexed)"),
        perPage: z
          .number()
          .optional()
          .default(100)
          .describe("Number of results per page"),
      },
      {
        readOnlyHint: true,
        destructiveHint: false,
        idempotentHint: true,
        openWorldHint: true,
      },
      async ({ query, searchIn = "all", page = 0, perPage = 100 }) => {
        // Empty query is allowed - useful for listing files
        const repoQualifier = `repo:${this.config.owner}/${this.config.repo}`;
    
        // Build search query based on searchIn parameter
        let qualifiedQuery: string;
    
        if (searchIn === "filename") {
          // Search for exact filename matches
          qualifiedQuery = `filename:${
            query.includes(" ") ? `"${query}"` : query
          } ${repoQualifier}`;
        } else if (searchIn === "path") {
          // Search anywhere in the file path. The `in:path` qualifier searches for the
          // query term within the file path.
          qualifiedQuery = `${query} in:path ${repoQualifier}`;
        } else if (searchIn === "content") {
          // Search only in file contents. This is the default behavior without qualifiers.
          qualifiedQuery = `${query} ${repoQualifier}`;
        } else {
          // "all" - comprehensive search. The GitHub search API (legacy) does not
          // support OR operators. The best we can do in a single query is to search
          // in file content and file path. The `in:file,path` qualifier does this.
          // This will match the query term if it appears in the content or anywhere
          // in the full path of a file, which includes the filename.
          qualifiedQuery = `${query} in:file,path ${repoQualifier}`;
        }
    
        let searchResults: {
          items: Array<{ name: string; path: string }>;
          total_count: number;
        };
        try {
          searchResults = await this.handleRequest(async () => {
            return this.octokit.search.code({
              q: qualifiedQuery,
              page,
              per_page: perPage,
            });
          });
        } catch (error) {
          // Enhanced error messages with specific GitHub search issues
          const errorMessage =
            error instanceof Error ? error.message : String(error);
          if (errorMessage.includes("validation failed")) {
            throw new Error(
              `GitHub search query invalid: "${qualifiedQuery}". Try simpler terms or check syntax.`
            );
          }
          if (errorMessage.includes("rate limit")) {
            throw new Error(
              "GitHub code search rate limit exceeded. Wait a moment and try again."
            );
          }
          if (
            errorMessage.includes("Forbidden") ||
            errorMessage.includes("401")
          ) {
            throw new Error(
              "GitHub API access denied. Check that your token has 'repo' scope for private repositories."
            );
          }
          throw error; // Re-throw other errors
        }
    
        // Enhanced formatting with file sizes and relevance indicators
        const formattedResults = searchResults.items
          .map((item) => {
            const fileName = item.name;
            const filePath = item.path;
            // const score = item.score || 0; // Could be used for relevance ranking in future
    
            // Determine why this file matched
            let matchReason = "";
            if (searchIn === "filename") {
              matchReason = "📝 filename match";
            } else if (searchIn === "path") {
              matchReason = "📁 path match";
            } else if (searchIn === "content") {
              matchReason = "📄 content match";
            } else {
              // searchIn is 'all', so we deduce the reason
              if (fileName.toLowerCase().includes(query.toLowerCase())) {
                matchReason = "📝 filename match";
              } else if (filePath.toLowerCase().includes(query.toLowerCase())) {
                matchReason = "📁 path match";
              } else {
                matchReason = "📄 content match";
              }
            }
    
            return `- **${fileName}** (${filePath}) ${matchReason}`;
          })
          .join("\n");
    
        let resultText = `Found ${searchResults.total_count} files`;
        if (searchIn !== "all") {
          resultText += ` searching in ${searchIn}`;
        }
        resultText += `:\n\n${formattedResults}`;
    
        // If no results, run diagnostics and provide enhanced response
        if (searchResults.total_count === 0) {
          const diagnostics = await this.runSearchDiagnostics(query);
          return this.formatNoResultsResponse(
            searchResults,
            diagnostics,
            query,
            searchIn
          );
        }
    
        return {
          content: [
            {
              type: "text" as const,
              text: resultText,
            },
          ],
        };
      }
    );
  • Helper method called by handler when no results; performs repository diagnostics including size check, search test, indexing status.
    private async runSearchDiagnostics(_originalQuery: string): Promise<{
      repoSize?: number;
      repoSizeGB?: number;
      isPrivate?: boolean;
      defaultBranch?: string;
      basicSearchWorks?: boolean;
      filesFound?: number;
      repoIndexed?: boolean;
      isLarge?: boolean;
      diagnosticError?: string;
    }> {
      try {
        // Test 1: Repository accessibility
        const repoInfo = await this.handleRequest(async () => {
          return this.octokit.repos.get({
            owner: this.config.owner,
            repo: this.config.repo,
          });
        });
    
        // Test 2: Basic search functionality with simple query
        let basicSearchWorks = false;
        let basicSearchCount = 0;
        try {
          const basicTest = await this.handleRequest(async () => {
            return this.octokit.search.code({
              q: `repo:${this.config.owner}/${this.config.repo} extension:md`,
              per_page: 1,
            });
          });
          basicSearchWorks = true;
          basicSearchCount = basicTest.total_count;
        } catch (_error) {
          basicSearchWorks = false;
        }
    
        const repoSizeKB = repoInfo.size;
        const repoSizeGB = repoSizeKB / (1024 * 1024);
        const isLarge = repoSizeGB > 50;
    
        return {
          repoSize: repoSizeKB,
          repoSizeGB: repoSizeGB,
          isPrivate: repoInfo.private,
          defaultBranch: repoInfo.default_branch,
          basicSearchWorks,
          filesFound: basicSearchCount,
          repoIndexed: basicSearchWorks && basicSearchCount > 0,
          isLarge,
        };
      } catch (error) {
        return {
          diagnosticError: error instanceof Error ? error.message : String(error),
        };
      }
    }
  • Helper method formats user-friendly response with search tips and diagnostic info when search yields zero results.
    private formatNoResultsResponse(
      _searchResults: { total_count: number },
      diagnostics: {
        diagnosticError?: string;
        repoIndexed?: boolean;
        isLarge?: boolean;
        repoSizeGB?: number;
        isPrivate?: boolean;
        defaultBranch?: string;
        filesFound?: number;
      },
      query: string,
      searchIn: string
    ): { content: Array<{ type: "text"; text: string }> } {
      let resultText = `Found 0 files matching "${query}"`;
      if (searchIn !== "all") {
        resultText += ` in ${searchIn}`;
      }
      resultText += "\n\n";
    
      if (diagnostics.diagnosticError) {
        resultText += `⚠️ **Search System Issue**: ${diagnostics.diagnosticError}\n\n`;
      } else if (!diagnostics.repoIndexed) {
        resultText +=
          "⚠️ **Repository May Not Be Indexed**: GitHub might not have indexed this repository for search.\n";
        resultText += "This can happen with:\n";
        resultText += "- New repositories (indexing takes time)\n";
        if (diagnostics.isLarge) {
          resultText += `- Large repositories (${diagnostics.repoSizeGB?.toFixed(
            2
          )} GB exceeds 50 GB limit)\n`;
        }
        if (diagnostics.isPrivate) {
          resultText += "- Private repositories with indexing issues\n";
        }
        resultText += "\n**Try**:\n";
        resultText += "- Search directly on GitHub.com to confirm\n";
        resultText +=
          "- Use the diagnoseSearch tool for detailed diagnostics\n\n";
      } else {
        resultText += "📊 **Search Debug Info**:\n";
        resultText += `- Repository: ${
          diagnostics.isPrivate ? "Private" : "Public"
        } (${diagnostics.repoSizeGB?.toFixed(3)} GB)\n`;
        resultText += `- Default branch: ${diagnostics.defaultBranch} (only branch searchable)\n`;
        resultText += `- Files in repo: ${diagnostics.filesFound} found\n`;
        resultText += `- Search query used: \`${query}\`\n\n`;
    
        resultText += "**Possible reasons for no results**:\n";
        resultText += "- The search term doesn't exist in the repository\n";
        resultText +=
          "- Content might be in non-default branches (not searchable)\n";
        resultText += "- Files might be larger than 384 KB (not indexed)\n\n";
      }
    
      // Add search tips
      resultText += "💡 **Search Tips:**\n";
      resultText += `- Try \`searchIn: "filename"\` to search only filenames\n`;
      resultText += `- Try \`searchIn: "path"\` to search file paths\n`;
      resultText += `- Try \`searchIn: "content"\` to search file contents\n`;
      resultText += `- Use quotes for exact phrases: "exact phrase"\n`;
      resultText += "- Use wildcards: `*.md` for markdown files\n";
      resultText += "- Try simpler or partial search terms";
    
      return {
        content: [
          {
            type: "text" as const,
            text: resultText,
          },
        ],
      };
    }
Behavior2/5

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

No annotations are provided, so the description carries the full burden. It mentions GitHub search syntax and supports searching in filenames, paths, and content, but lacks details on permissions, rate limits, pagination behavior (beyond schema hints), or what the response looks like. For a search tool with zero annotation coverage, this is insufficient.

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 front-loaded with the core purpose and efficiently covers key features in two sentences. It avoids redundancy, though it could be slightly more structured (e.g., separating syntax support from search scope).

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

Completeness2/5

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

Given no annotations and no output schema, the description is incomplete for a search tool with 4 parameters. It doesn't explain the return format, error handling, or behavioral traits like rate limits, leaving significant gaps for the agent to understand tool behavior.

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?

Schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds minimal value by mentioning GitHub's search syntax and 'comprehensive search' for the 'all' option, but doesn't provide additional syntax examples or constraints beyond what the schema specifies.

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

Purpose4/5

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

The description clearly states the tool searches for notes, documents, and files within a specific Obsidian vault on GitHub, using GitHub's search syntax. It specifies the resource (files in a vault) and verb (search), but doesn't explicitly differentiate from sibling tools like searchIssues, which might search different content types.

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

Usage Guidelines2/5

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

The description mentions searching for knowledge base content, implying usage for finding files, but provides no explicit guidance on when to use this tool versus alternatives like getFileContents or searchIssues. No exclusions or prerequisites are stated, leaving the agent to infer context.

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/Hint-Services/obsidian-github-mcp'

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