Skip to main content
Glama

search_codebase

Search your indexed codebase to find relevant code patterns, usage examples, and architectural context. Supports intents for edit, refactor, or migrate with preflight readiness checks.

Instructions

Routes to the active/current project automatically when known. Search the indexed codebase. Default compact mode returns at most 6 ranked results with light graph context (importedByCount, topExports, layer), a patternSummary, bestExample, nextHops, and response-budget metadata. Use mode="full" for today's richer response with full hints arrays and all memories — identical shape as before this parameter existed. IMPORTANT: Pass the intent="edit"|"refactor"|"migrate" to get preflight: edit readiness check with evidence gating.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesNatural language search query
modeNoResponse mode. compact (default): max 6 results with light graph context, pattern summary, best example, next hops, and budget metadata. full: today's richer shape + budget metadata.compact
intentNoOptional. Use "edit", "refactor", or "migrate" to get the full preflight card before making changes.
limitNoMaximum number of results to return (default: 5)
includeSnippetsNoInclude code snippets in results (default: false). If you need code, prefer read_file instead.
filtersNoOptional filters
projectNoOptional project selector for this call. Accepts a project root path, file path, file:// URI, or a relative subproject path under a configured root.
project_directoryNoDeprecated compatibility alias for older clients. Prefer project.

Implementation Reference

  • The main handler function `handle()` that executes the search_codebase tool logic. It accepts query, limit, filters, intent, includeSnippets, and mode arguments, performs search via CodebaseSearcher, loads intelligence/relationships/health data, computes preflight decision cards (for edit/refactor/migrate intents), builds compact or full response payload with memories, health, pattern summaries, best examples, next hops, and returns the formatted response.
    export async function handle(
      args: Record<string, unknown>,
      ctx: ToolContext
    ): Promise<ToolResponse> {
      const { query, limit, filters, intent, includeSnippets, mode } = args as {
        query?: unknown;
        limit?: number;
        filters?: Record<string, unknown>;
        intent?: string;
        includeSnippets?: boolean;
        mode?: string;
      };
      const queryStr = typeof query === 'string' ? query.trim() : '';
    
      if (!queryStr) {
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(
                {
                  status: 'error',
                  errorCode: 'invalid_params',
                  message: "Invalid params: 'query' is required and must be a non-empty string.",
                  hint: "Provide a query like 'how are routes configured' or 'AlbumApiService'."
                },
                null,
                2
              )
            }
          ],
          isError: true
        };
      }
    
      if (ctx.indexState.status === 'indexing') {
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(
                {
                  status: 'indexing',
                  message: 'Index is still being built. Retry in a moment.',
                  progress: ctx.indexState.indexer?.getProgress()
                },
                null,
                2
              )
            }
          ]
        };
      }
    
      if (ctx.indexState.status === 'error') {
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(
                {
                  status: 'error',
                  message: `Indexing failed: ${ctx.indexState.error}`
                },
                null,
                2
              )
            }
          ]
        };
      }
    
      const searcher = new CodebaseSearcher(ctx.rootPath);
      let results: SearchResult[];
      const searchProfile = (
        intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent) ? intent : 'explore'
      ) as SearchIntentProfile;
    
      try {
        results = await searcher.search(queryStr, limit || 5, filters, {
          profile: searchProfile
        });
      } catch (error) {
        if (error instanceof IndexCorruptedError) {
          console.error('[Auto-Heal] Index corrupted. Triggering background re-index...');
          void ctx.performIndexing();
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(
                  {
                    status: 'indexing',
                    message: 'Index was corrupt. Rebuild started — retry shortly.'
                  },
                  null,
                  2
                )
              }
            ]
          };
        }
        throw error;
      }
    
      // Load memories for keyword matching, enriched with confidence
      const allMemories = await readMemoriesFile(ctx.paths.memory);
      const allMemoriesWithConf = withConfidence(allMemories);
      const queryTerms = queryStr.toLowerCase().split(/\s+/).filter(Boolean);
      const queryTermSet = new Set(queryTerms);
    
      // Load intelligence data for enrichment (all intents, not just preflight)
      let intelligence: IntelligenceData | null = null;
      try {
        const intelligenceContent = await fs.readFile(ctx.paths.intelligence, 'utf-8');
        const parsed = JSON.parse(intelligenceContent) as unknown;
        if (typeof parsed === 'object' && parsed !== null) {
          intelligence = parsed as IntelligenceData;
        }
      } catch {
        /* graceful degradation — intelligence file may not exist yet */
      }
    
      // Load relationships sidecar (preferred over intelligence.internalFileGraph)
      let relationships: RelationshipsData | null = null;
      try {
        const relationshipsPath = path.join(
          path.dirname(ctx.paths.intelligence),
          RELATIONSHIPS_FILENAME
        );
        const relationshipsContent = await fs.readFile(relationshipsPath, 'utf-8');
        const parsed = JSON.parse(relationshipsContent) as unknown;
        if (typeof parsed === 'object' && parsed !== null) {
          relationships = parsed as RelationshipsData;
        }
      } catch {
        /* graceful degradation — relationships sidecar may not exist yet */
      }
    
      const healthArtifact = await readHealthFile(ctx.paths.health);
      const healthByFile = indexHealthByFile(healthArtifact, ctx.rootPath);
    
      // Helper to get imports graph from relationships sidecar (preferred) or intelligence
      function getImportsGraph(): Record<string, string[]> | null {
        if (relationships?.graph?.imports) {
          return relationships.graph.imports as Record<string, string[]>;
        }
        if (intelligence?.internalFileGraph?.imports) {
          return intelligence.internalFileGraph.imports as Record<string, string[]>;
        }
        return null;
      }
    
      type ImportEdgeDetail = { line?: number; importedSymbols?: string[] };
      type ImportDetailsGraph = Record<string, Record<string, ImportEdgeDetail>>;
    
      function getImportDetailsGraph(): ImportDetailsGraph | null {
        if (relationships?.graph?.importDetails) {
          return relationships.graph.importDetails as ImportDetailsGraph;
        }
        const internalDetails = intelligence?.internalFileGraph?.importDetails;
        if (internalDetails) {
          return internalDetails as ImportDetailsGraph;
        }
        return null;
      }
    
      function normalizeGraphPath(filePath: string): string {
        const normalized = filePath.replace(/\\/g, '/');
        if (path.isAbsolute(filePath)) {
          const rel = path.relative(ctx.rootPath, filePath).replace(/\\/g, '/');
          if (rel && !rel.startsWith('..')) {
            return rel;
          }
        }
        return normalized.replace(/^\.\//, '');
      }
    
      function normalizeSymbolName(value: string): string {
        return value.trim().toLowerCase();
      }
    
      function pathsMatch(a: string, b: string): boolean {
        return a === b || a.endsWith(b) || b.endsWith(a);
      }
    
      const resultPathSet = new Set(results.map((result) => normalizeGraphPath(result.filePath)));
      const resultSymbolSet = new Set(
        results
          .map((result) => {
            const symbolName = result.metadata?.symbolName;
            return typeof symbolName === 'string' ? normalizeSymbolName(symbolName) : null;
          })
          .filter((value): value is string => value !== null)
      );
    
      function getMemoryScopeBoost(memory: MemoryWithConfidence): number {
        if (!memory.scope || memory.scope.kind === 'global') return 0;
    
        const normalizedFile = normalizeGraphPath(memory.scope.file);
        if (memory.scope.kind === 'file') {
          return resultPathSet.has(normalizedFile) ? 3 : 0;
        }
    
        const symbolMatch =
          resultSymbolSet.has(normalizeSymbolName(memory.scope.symbol)) ||
          queryTermSet.has(normalizeSymbolName(memory.scope.symbol));
    
        if (resultPathSet.has(normalizedFile) && symbolMatch) return 4;
        if (resultPathSet.has(normalizedFile)) return 2;
        if (symbolMatch) return 1;
        return 0;
      }
    
      function getMemoryTextMatchCount(memory: MemoryWithConfidence): number {
        const haystack =
          `${memory.memory} ${memory.reason} ${formatMemoryScopeText(memory.scope)}`.toLowerCase();
        return queryTerms.filter((term) => haystack.includes(term)).length;
      }
    
      function formatMemoryForOutput(memory: MemoryWithConfidence): string {
        const scopeText =
          !memory.scope || memory.scope.kind === 'global'
            ? ''
            : memory.scope.kind === 'file'
              ? ` [${memory.scope.file}]`
              : ` [${memory.scope.file}#${memory.scope.symbol}]`;
        return `${memory.memory}${scopeText} (${memory.effectiveConfidence})`;
      }
    
      const relatedMemories = allMemoriesWithConf
        .map((memory) => ({
          memory,
          textMatches: getMemoryTextMatchCount(memory),
          scopeBoost: getMemoryScopeBoost(memory)
        }))
        .filter((entry) => entry.textMatches > 0 || entry.scopeBoost > 0)
        .sort((a, b) => {
          if (b.scopeBoost !== a.scopeBoost) return b.scopeBoost - a.scopeBoost;
          if (b.textMatches !== a.textMatches) return b.textMatches - a.textMatches;
          return b.memory.effectiveConfidence - a.memory.effectiveConfidence;
        })
        .map((entry) => entry.memory);
    
      function computeIndexConfidence(): 'fresh' | 'aging' | 'stale' {
        let confidence: 'fresh' | 'aging' | 'stale' = 'stale';
        if (intelligence?.generatedAt) {
          const indexAge = Date.now() - new Date(intelligence.generatedAt).getTime();
          const hoursOld = indexAge / (1000 * 60 * 60);
          if (hoursOld < 24) {
            confidence = 'fresh';
          } else if (hoursOld < 168) {
            confidence = 'aging';
          }
        }
        return confidence;
      }
    
      type ImpactCandidate = { file: string; line?: number; hop: 1 | 2 };
    
      function findImportDetail(
        details: ImportDetailsGraph | null,
        importer: string,
        imported: string
      ): ImportEdgeDetail | null {
        if (!details) return null;
        const edges = details[importer];
        if (!edges) return null;
        if (edges[imported]) return edges[imported];
    
        let bestKey: string | null = null;
        for (const depKey of Object.keys(edges)) {
          if (!pathsMatch(depKey, imported)) continue;
          if (!bestKey || depKey.length > bestKey.length) {
            bestKey = depKey;
          }
        }
    
        return bestKey ? edges[bestKey] : null;
      }
    
      // Impact breadth estimate from the import graph (used for risk assessment).
      // 2-hop: direct importers (hop 1) + importers of importers (hop 2).
      function computeImpactCandidates(resultPaths: string[]): ImpactCandidate[] {
        const allImports = getImportsGraph();
        if (!allImports) return [];
    
        const importDetails = getImportDetailsGraph();
    
        const reverseImportsLocal = new Map<string, string[]>();
        for (const [file, deps] of Object.entries<string[]>(allImports)) {
          for (const dep of deps) {
            if (!reverseImportsLocal.has(dep)) reverseImportsLocal.set(dep, []);
            reverseImportsLocal.get(dep)!.push(file);
          }
        }
    
        const targets = resultPaths.map((rp) => normalizeGraphPath(rp));
    
        const candidates = new Map<string, ImpactCandidate>();
    
        const addCandidate = (file: string, hop: 1 | 2, line?: number): void => {
          const existing = candidates.get(file);
          if (existing) {
            if (existing.hop <= hop) return;
          }
          candidates.set(file, { file, hop, ...(line ? { line } : {}) });
        };
    
        const collectImporters = (
          target: string
        ): Array<{ importer: string; detail: ImportEdgeDetail | null }> => {
          const matches: Array<{ importer: string; detail: ImportEdgeDetail | null }> = [];
          for (const [dep, importers] of reverseImportsLocal) {
            if (!pathsMatch(dep, target)) continue;
            for (const importer of importers) {
              matches.push({ importer, detail: findImportDetail(importDetails, importer, dep) });
            }
          }
          return matches;
        };
    
        // Hop 1
        const hop1Files: string[] = [];
        for (const target of targets) {
          for (const { importer, detail } of collectImporters(target)) {
            addCandidate(importer, 1, detail?.line);
          }
        }
        for (const candidate of candidates.values()) {
          if (candidate.hop === 1) hop1Files.push(candidate.file);
        }
    
        // Hop 2
        for (const mid of hop1Files) {
          for (const { importer, detail } of collectImporters(mid)) {
            addCandidate(importer, 2, detail?.line);
          }
        }
    
        return Array.from(candidates.values()).slice(0, 20);
      }
    
      // Build reverse import map: prefer pre-computed relationships.graph.importedBy,
      // fall back to rebuilding from graph.imports for older index formats.
      const reverseImports = new Map<string, string[]>();
      if (relationships?.graph?.importedBy) {
        for (const [dep, importers] of Object.entries<string[]>(relationships.graph.importedBy)) {
          reverseImports.set(dep, importers);
        }
      } else {
        const importsGraph = getImportsGraph();
        if (importsGraph) {
          for (const [file, deps] of Object.entries<string[]>(importsGraph)) {
            for (const dep of deps) {
              if (!reverseImports.has(dep)) reverseImports.set(dep, []);
              reverseImports.get(dep)!.push(file);
            }
          }
        }
      }
    
      // Build relationship hints with capped arrays ranked by importedByCount
      interface RelationshipHints {
        relationships?: {
          importedByCount?: number;
          hasTests?: boolean;
        };
        hints?: {
          callers?: string[];
          tests?: string[];
        };
      }
    
      function buildRelationshipHints(result: SearchResult): RelationshipHints {
        const rPath = result.filePath;
        // Graph keys are relative paths with forward slashes; normalize for comparison
        const rPathNorm =
          path.relative(ctx.rootPath, rPath).replace(/\\/g, '/') || rPath.replace(/\\/g, '/');
    
        // importedBy: files that import this result (reverse lookup), collect with counts
        const importedByMap = new Map<string, number>();
        for (const [dep, importers] of reverseImports) {
          if (dep === rPathNorm || dep.endsWith(rPathNorm) || rPathNorm.endsWith(dep)) {
            for (const importer of importers) {
              importedByMap.set(importer, (importedByMap.get(importer) || 0) + 1);
            }
          }
        }
    
        // testedIn: heuristic — same basename with .spec/.test extension
        const testedIn: string[] = [];
        const baseName = path.basename(rPathNorm).replace(/\.[^.]+$/, '');
        const localImportsGraph = getImportsGraph();
        if (localImportsGraph) {
          for (const file of Object.keys(localImportsGraph)) {
            const fileBase = path.basename(file);
            if (
              (fileBase.includes('.spec.') || fileBase.includes('.test.')) &&
              fileBase.startsWith(baseName)
            ) {
              testedIn.push(file);
            }
          }
        }
    
        // Build condensed relationships
        const condensedRel: Record<string, number | boolean> = {};
        if (importedByMap.size > 0) {
          condensedRel.importedByCount = importedByMap.size;
        }
        if (testedIn.length > 0) {
          condensedRel.hasTests = true;
        }
    
        // Build hints object with capped arrays
        const hintsObj: Record<string, string[]> = {};
    
        // Rank importers by count descending, cap at 3
        if (importedByMap.size > 0) {
          const sortedCallers = Array.from(importedByMap.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, 3)
            .map(([file]) => file);
          hintsObj.callers = sortedCallers;
          // NOTE: consumers removed — it was identical to callers (pure token waste)
        }
    
        // Cap tests at 3
        if (testedIn.length > 0) {
          hintsObj.tests = testedIn.slice(0, 3);
        }
    
        // Return both condensed and hints (hints only included if non-empty)
        const output: RelationshipHints = {};
        if (Object.keys(condensedRel).length > 0) {
          output.relationships = condensedRel as {
            importedByCount?: number;
            hasTests?: boolean;
          };
        }
        if (Object.keys(hintsObj).length > 0) {
          output.hints = hintsObj as {
            callers?: string[];
            tests?: string[];
          };
        }
    
        return output;
      }
    
      // Get the count of files that import a given result (compact mode graph context)
      function getImportedByCount(result: SearchResult): number {
        const rPathNorm = normalizeGraphPath(result.filePath);
        const importers = new Set<string>();
        for (const [dep, imps] of reverseImports) {
          if (dep === rPathNorm || dep.endsWith(rPathNorm) || rPathNorm.endsWith(dep)) {
            for (const imp of imps) importers.add(imp);
          }
        }
        return importers.size;
      }
    
      // Get up to maxCount named exports for a file from relationships.graph.exports
      function getTopExports(filePath: string, maxCount = 3): string[] {
        if (!relationships?.graph?.exports) return [];
        const normalized = normalizeGraphPath(filePath);
        const fileExports = relationships.graph.exports[normalized];
        if (!Array.isArray(fileExports)) return [];
        return fileExports
          .filter((e) => e.name !== 'default')
          .slice(0, maxCount)
          .map((e) => e.name);
      }
    
      // Filter memories to only strongly relevant ones for compact mode:
      // ≥2 non-stop-word query term matches AND effectiveConfidence ≥ 0.5
      function filterStrongMemories(
        memories: MemoryWithConfidence[],
        query: string
      ): MemoryWithConfidence[] {
        const terms = query
          .toLowerCase()
          .split(/\s+/)
          .filter((t) => t.length > 2 && !COMPACT_STOP_WORDS.has(t));
        if (terms.length === 0) return [];
        return memories
          .filter((m) => {
            const text = `${m.memory} ${m.reason} ${formatMemoryScopeText(m.scope)}`.toLowerCase();
            const matchCount = terms.filter((t) => text.includes(t)).length;
            return (matchCount >= 2 || getMemoryScopeBoost(m) >= 2) && m.effectiveConfidence >= 0.5;
          })
          .slice(0, 2);
      }
    
      function getResultHealth(
        filePath: string
      ): { level: 'low' | 'medium' | 'high'; reasons?: string[] } | undefined {
        const fileHealth = healthByFile.get(normalizeHealthLookupKey(filePath, ctx.rootPath));
        if (!fileHealth || fileHealth.level === 'low') {
          return undefined;
        }
        return {
          level: fileHealth.level,
          ...(fileHealth.reasons.length > 0 && { reasons: fileHealth.reasons.slice(0, 2) })
        };
      }
    
      function summarizeResultHealth(
        resultPaths: string[]
      ): { level: 'low' | 'medium' | 'high'; reasons?: string[] } | undefined {
        const matched = resultPaths
          .map((filePath) => healthByFile.get(normalizeHealthLookupKey(filePath, ctx.rootPath)))
          .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry));
        if (matched.length === 0) {
          return undefined;
        }
    
        const priority = { high: 3, medium: 2, low: 1 };
        matched.sort((a, b) => {
          if (priority[b.level] !== priority[a.level]) return priority[b.level] - priority[a.level];
          if (b.score !== a.score) return b.score - a.score;
          return a.file.localeCompare(b.file);
        });
    
        const top = matched[0];
        const reasons = [...top.reasons];
        const sameLevelCount = matched.filter((entry) => entry.level === top.level).length;
        if (sameLevelCount > 1) {
          reasons.push(`${sameLevelCount} result files are marked ${top.level}-risk`);
        }
    
        return {
          level: top.level,
          ...(reasons.length > 0 && { reasons: reasons.slice(0, 3) })
        };
      }
    
      // Build a 1-line pattern summary string from intelligence.json patterns (compact mode)
      function buildPatternSummary(): string | undefined {
        const patterns = intelligence?.patterns;
        if (!patterns) return undefined;
        const entries = Object.values(patterns)
          .filter((data) => Boolean(data.primary))
          .slice(0, 3)
          .map((data) => {
            const p = data.primary!;
            return `${p.name} (${p.frequency}, ${p.trend ?? 'Stable'})`;
          });
        return entries.length > 0 ? entries.join(', ') : undefined;
      }
    
      // Return the golden file path most relevant to the given search results, using directory
      // overlap as a query-aware proximity signal. Falls back to goldenFiles[0] when no overlap.
      // Track B: use cross-encoder reranker to score golden files against the query for stronger
      // relevance — deferred to avoid reranker latency on compact mode in v2.
      function getBestExample(topResults: Array<{ filePath: string }>): string | undefined {
        const goldenFiles = intelligence?.goldenFiles;
        if (!goldenFiles?.length) return undefined;
        if (!topResults?.length) return goldenFiles[0].file;
    
        // Extract directory paths from top-5 results
        const resultDirs = topResults.slice(0, 5).map((r) => path.dirname(r.filePath));
    
        let bestFile = goldenFiles[0];
        let bestScore = -1;
    
        for (const gf of goldenFiles) {
          const gfParts = path.dirname(gf.file).split(path.sep).filter(Boolean);
          let maxShared = 0;
    
          for (const resDir of resultDirs) {
            const resParts = resDir.split(path.sep).filter(Boolean);
            let shared = 0;
            const minLen = Math.min(gfParts.length, resParts.length);
            for (let i = 0; i < minLen; i++) {
              if (gfParts[i] === resParts[i]) shared++;
              else break;
            }
            if (shared > maxShared) maxShared = shared;
          }
    
          // Use golden file score as tie-breaker
          const score = maxShared * 1000 + (gf.score ?? 0);
          if (score > bestScore) {
            bestScore = score;
            bestFile = gf;
          }
        }
    
        return bestFile.file;
      }
    
      // Build up to 2 dynamic next-hop suggestions (compact mode)
      function buildNextHops(
        compactResults: SearchResult[],
        sq: { status: string }
      ): Array<{ tool: string; args?: Record<string, string>; why: string }> {
        const hops: Array<{ tool: string; args?: Record<string, string>; why: string }> = [];
        if (compactResults.length > 0) {
          hops.push({
            tool: 'read_file',
            args: { path: compactResults[0].filePath },
            why: 'Read the top match for implementation details'
          });
        }
        if (sq.status === 'low_confidence') {
          hops.push({ tool: 'get_team_patterns', why: 'Review pattern guidance — confidence is low' });
        } else if (hops.length < 2) {
          hops.push({ tool: 'get_team_patterns', why: 'Review team patterns and best examples' });
        }
        return hops.slice(0, 2);
      }
    
      const searchQuality = assessSearchQuality(queryStr, results);
    
      // Always-on edit preflight (lite): do not require intent and keep payload small.
      let editPreflight:
        | { mode: string; riskLevel: string; confidence: string; evidenceLock: EvidenceLock }
        | undefined = undefined;
      if (intelligence && (!intent || intent === 'explore')) {
        try {
          const resultPaths = results.map((r) => r.filePath);
          const impactCandidates = computeImpactCandidates(resultPaths);
    
          // Use existing pattern intelligence for evidenceLock scoring, but keep the output payload lite.
          const preferredPatternsForEvidence: Array<{ pattern: string; example?: string }> = [];
          const patterns: PatternsData = intelligence.patterns || {};
          for (const [_, data] of Object.entries(patterns)) {
            if (data.primary) {
              const p = data.primary;
              if (p.trend === 'Rising' || p.trend === 'Stable') {
                preferredPatternsForEvidence.push({
                  pattern: p.name,
                  ...(p.canonicalExample && { example: p.canonicalExample.file })
                });
              }
            }
          }
    
          let riskLevel: 'low' | 'medium' | 'high' = 'low';
          if (impactCandidates.length > 10) {
            riskLevel = 'high';
          } else if (impactCandidates.length > 3) {
            riskLevel = 'medium';
          }
    
          editPreflight = {
            mode: 'lite',
            riskLevel,
            confidence: computeIndexConfidence(),
            evidenceLock: buildEvidenceLock({
              results,
              preferredPatterns: preferredPatternsForEvidence.slice(0, 5),
              relatedMemories,
              failureWarnings: [],
              patternConflicts: [],
              searchQualityStatus: searchQuality.status
              // Note: indexFreshness intentionally omitted — explore intent is for navigation,
              // not mutation; freshness gating does not apply.
            })
          };
        } catch {
          // editPreflight is best-effort - never fail search over it
        }
      }
    
      // Compose preflight card for edit/refactor/migrate intents
      let preflight: DecisionCard | undefined = undefined;
      const preflightIntents = ['edit', 'refactor', 'migrate'];
      if (intent && preflightIntents.includes(intent)) {
        if (!intelligence) {
          preflight = {
            ready: false,
            nextAction: 'Run a full index rebuild to generate pattern intelligence before editing.'
          };
        } else {
          try {
            // --- Avoid / Prefer patterns ---
            const avoidPatternsList: Array<{
              pattern: string;
              category: string;
              adoption: string;
              trend: string;
              guidance?: string;
            }> = [];
            const preferredPatternsList: Array<{
              pattern: string;
              category: string;
              adoption: string;
              trend: string;
              guidance?: string;
              example?: string;
            }> = [];
            const patterns: PatternsData = intelligence.patterns || {};
            for (const [category, data] of Object.entries(patterns)) {
              // Primary pattern = preferred if Rising or Stable
              if (data.primary) {
                const p = data.primary;
                if (p.trend === 'Rising' || p.trend === 'Stable') {
                  preferredPatternsList.push({
                    pattern: p.name,
                    category,
                    adoption: p.frequency,
                    trend: p.trend,
                    guidance: p.guidance,
                    ...(p.canonicalExample && { example: p.canonicalExample.file })
                  });
                }
              }
              // Also-detected patterns that are Declining = avoid
              if (data.alsoDetected) {
                for (const alt of data.alsoDetected) {
                  if (alt.trend === 'Declining') {
                    avoidPatternsList.push({
                      pattern: alt.name,
                      category,
                      adoption: alt.frequency,
                      trend: 'Declining',
                      guidance: alt.guidance
                    });
                  }
                }
              }
            }
    
            // --- Impact candidates (files importing the result files) ---
            const resultPaths = results.map((r) => r.filePath);
            const impactCandidates = computeImpactCandidates(resultPaths);
    
            // PREF-02: Compute impact coverage (callers of result files that appear in results)
            const callerFiles = resultPaths.flatMap((p) => {
              const importers: string[] = [];
              for (const [dep, importerList] of reverseImports) {
                if (dep.endsWith(p) || p.endsWith(dep)) {
                  importers.push(...importerList);
                }
              }
              return importers;
            });
            const uniqueCallers = new Set(callerFiles);
            const callersCovered = Array.from(uniqueCallers).filter((f) =>
              resultPaths.some((rp) => f.endsWith(rp) || rp.endsWith(f))
            ).length;
            const callersTotal = uniqueCallers.size;
            const impactCoverage =
              callersTotal > 0 ? { covered: callersCovered, total: callersTotal } : undefined;
    
            // --- Risk level (based on circular deps + impact breadth) ---
            //TODO: Review this risk level calculation
            let riskLevel: 'low' | 'medium' | 'high' = 'low';
            let cycleCount = 0;
            const graphDataSource = relationships?.graph || intelligence?.internalFileGraph;
            if (graphDataSource) {
              try {
                const graph = InternalFileGraph.fromJSON(
                  graphDataSource as {
                    imports?: Record<string, string[]>;
                    exports?: Record<string, FileExport[]>;
                    importDetails?: Record<
                      string,
                      Record<string, { line?: number; importedSymbols?: string[] }>
                    >;
                  },
                  ctx.rootPath
                );
                // Use directory prefixes as scope (not full file paths)
                // findCycles(scope) filters files by startsWith, so a full path would only match itself
                const scopes = new Set(
                  resultPaths.map((rp) => {
                    const lastSlash = rp.lastIndexOf('/');
                    return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp;
                  })
                );
                for (const scope of scopes) {
                  const cycles = graph.findCycles(scope);
                  cycleCount += cycles.length;
                }
              } catch {
                // Graph reconstruction failed — skip cycle check
              }
            }
            if (cycleCount > 0 || impactCandidates.length > 10) {
              riskLevel = 'high';
            } else if (impactCandidates.length > 3) {
              riskLevel = 'medium';
            }
    
            // --- Golden files (exemplar code) ---
            const goldenFiles = (intelligence.goldenFiles ?? [])
              .slice(0, 3)
              .map((g: IntelligenceGoldenFile) => ({
                file: g.file,
                score: g.score
              }));
    
            // --- Confidence (index freshness) ---
            // TODO: Review this confidence calculation
            //const confidence = computeIndexConfidence();
    
            // --- Failure memories (1.5x relevance boost) ---
            const failureWarnings = relatedMemories
              .filter((m) => m.type === 'failure' && !m.stale)
              .map((m) => ({
                memory: m.memory,
                reason: m.reason,
                confidence: m.effectiveConfidence
              }))
              .slice(0, 3);
    
            const preferredPatternsForOutput = preferredPatternsList.slice(0, 5);
            const avoidPatternsForOutput = avoidPatternsList.slice(0, 5);
    
            // --- Pattern conflicts (split decisions within categories) ---
            const patternConflicts: Array<{
              category: string;
              primary: { name: string; adoption: string };
              alternative: { name: string; adoption: string };
            }> = [];
            const hasUnitTestFramework = Boolean(patterns.unitTestFramework?.primary);
            for (const [cat, data] of Object.entries(patterns)) {
              if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns)) continue;
              if (!shouldIncludePatternConflictCategory(cat, queryStr)) continue;
              if (!data.primary || !data.alsoDetected?.length) continue;
              const primaryFreq = parseFloat(data.primary.frequency) || 100;
              if (primaryFreq >= 80) continue;
              for (const alt of data.alsoDetected) {
                const altFreq = parseFloat(alt.frequency) || 0;
                if (altFreq >= 20) {
                  if (isComplementaryPatternConflict(cat, data.primary.name, alt.name)) continue;
                  if (hasUnitTestFramework && cat === 'testingFramework') continue;
                  patternConflicts.push({
                    category: cat,
                    primary: { name: data.primary.name, adoption: data.primary.frequency },
                    alternative: { name: alt.name, adoption: alt.frequency }
                  });
                }
              }
            }
    
            const evidenceLock = buildEvidenceLock({
              results,
              preferredPatterns: preferredPatternsForOutput,
              relatedMemories,
              failureWarnings,
              patternConflicts,
              searchQualityStatus: searchQuality.status,
              impactCoverage,
              indexFreshness: computeIndexConfidence()
            });
    
            // Build clean decision card (PREF-01 to PREF-04)
            const decisionCard: DecisionCard = {
              ready: evidenceLock.readyToEdit
            };
    
            // Add nextAction if not ready
            if (!decisionCard.ready && evidenceLock.nextAction) {
              decisionCard.nextAction = evidenceLock.nextAction;
            }
    
            // Add warnings from failure memories (capped at 3)
            if (failureWarnings.length > 0) {
              decisionCard.warnings = failureWarnings.slice(0, 3).map((w) => w.memory);
            }
    
            // Surface freshness gap warnings (aging/stale) into the decision card
            const freshnessGaps = (evidenceLock.gaps ?? []).filter(
              (g) => g.includes('aging') || g.includes('stale')
            );
            if (freshnessGaps.length > 0) {
              decisionCard.warnings = [...(decisionCard.warnings ?? []), ...freshnessGaps];
            }
    
            // Add patterns (do/avoid, capped at 3 each, with adoption %)
            const doPatterns = preferredPatternsForOutput
              .slice(0, 3)
              .map((p) => `${p.pattern} — ${p.adoption ? `${p.adoption} adoption` : ''}`);
            const avoidPatterns = avoidPatternsForOutput
              .slice(0, 3)
              .map((p) => `${p.pattern} — ${p.adoption ? `${p.adoption} adoption` : ''} (declining)`);
            if (doPatterns.length > 0 || avoidPatterns.length > 0) {
              decisionCard.patterns = {
                ...(doPatterns.length > 0 && { do: doPatterns }),
                ...(avoidPatterns.length > 0 && { avoid: avoidPatterns })
              };
            }
    
            // Add bestExample — query-aware via directory overlap against top search results
            const preflightBestExample = getBestExample(results.slice(0, 5));
            if (preflightBestExample) {
              decisionCard.bestExample = preflightBestExample;
            }
    
            // Add impact (coverage + top 3 files)
            if (impactCoverage || impactCandidates.length > 0) {
              const impactObj: {
                coverage?: string;
                files?: string[];
                details?: Array<{ file: string; line?: number; hop: 1 | 2 }>;
              } = {};
              if (impactCoverage) {
                impactObj.coverage = `${impactCoverage.covered}/${impactCoverage.total} callers in results`;
              }
              if (impactCandidates.length > 0) {
                const top = impactCandidates.slice(0, 3);
                impactObj.files = top.map((candidate) => candidate.file);
                impactObj.details = top;
              }
              if (Object.keys(impactObj).length > 0) {
                decisionCard.impact = impactObj;
              }
            }
    
            const healthSummary = summarizeResultHealth(resultPaths);
            if (healthSummary) {
              decisionCard.health = healthSummary;
            } else if (riskLevel !== 'low') {
              const reasons: string[] = [];
              if (cycleCount > 0) {
                reasons.push(
                  `${cycleCount} circular dependenc${cycleCount === 1 ? 'y' : 'ies'} in the result area`
                );
              }
              if (impactCandidates.length > 3) {
                reasons.push(`${impactCandidates.length} upstream callers may be affected`);
              }
              decisionCard.health = {
                level: riskLevel,
                ...(reasons.length > 0 && { reasons })
              };
            }
    
            // Add whatWouldHelp from evidenceLock
            if (evidenceLock.whatWouldHelp && evidenceLock.whatWouldHelp.length > 0) {
              decisionCard.whatWouldHelp = evidenceLock.whatWouldHelp;
            }
    
            // Soft-abstain: block status OR low-confidence search quality signals insufficient
            // evidence for edit guidance. Results are still returned (soft abstain per APPROACH Decision 2).
            if (evidenceLock.status === 'block' || searchQuality.status === 'low_confidence') {
              decisionCard.abstain = true;
            }
    
            preflight = decisionCard;
          } catch {
            // Preflight construction failed — skip preflight, don't fail the search
          }
        }
      }
    
      // For edit/refactor/migrate: return clean decision card.
      // For explore or lite-only: return lightweight { ready, reason }.
      let preflightPayload: { ready: boolean; reason?: string } | Record<string, unknown> | undefined;
      if (preflight) {
        // preflight is already a clean decision card (DecisionCard type)
        preflightPayload = preflight;
      } else if (editPreflight) {
        // Lite preflight for explore intent
        const el = editPreflight.evidenceLock;
        preflightPayload = {
          ready: el?.readyToEdit ?? false,
          ...(el && !el.readyToEdit && el.nextAction && { reason: el.nextAction })
        };
      }
    
      // Helper: Build scope header for symbol-aware chunks (SEARCH-02)
      function buildScopeHeader(metadata: ChunkMetadata): string | null {
        // Try symbolPath first (most reliable for AST-based symbols)
        if (metadata?.symbolPath && Array.isArray(metadata.symbolPath)) {
          return metadata.symbolPath.join('.');
        }
        // Fallback: className + functionName
        if (metadata?.className && metadata?.functionName) {
          return `${metadata.className}.${metadata.functionName}`;
        }
        // Class only
        if (metadata?.className) {
          return metadata.className;
        }
        // Function only
        if (metadata?.functionName) {
          return metadata.functionName;
        }
        // component chunk fallback (component or pipe name)
        if (metadata?.componentName) {
          return metadata.componentName;
        }
        return null;
      }
    
      function formatSnippetFallbackHeader(filePath: string, startLine: number): string {
        const rel = path.relative(ctx.rootPath, filePath).replace(/\\/g, '/');
        const displayPath =
          rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : path.basename(filePath);
        return `${displayPath}:${startLine}`;
      }
    
      function enrichSnippetWithScope(
        snippet: string | undefined,
        metadata: ChunkMetadata,
        filePath: string,
        startLine: number
      ): string | undefined {
        if (!snippet) return undefined;
    
        const cleanedSnippet = snippet.replace(/^\r?\n+/, '');
        if (cleanedSnippet.startsWith('//')) {
          return cleanedSnippet;
        }
    
        const scopeHeader =
          buildScopeHeader(metadata) ?? formatSnippetFallbackHeader(filePath, startLine);
        return `// ${scopeHeader}\n${cleanedSnippet}`;
      }
    
      const rerankerStatus = getRerankerStatus();
      const searchQualityBlock = {
        status: searchQuality.status,
        confidence: searchQuality.confidence,
        ...(searchQuality.status === 'low_confidence' &&
          searchQuality.nextSteps?.[0] && {
            hint: searchQuality.nextSteps[0]
          }),
        // Surface reranker health so agents know if they're getting degraded results
        ...(rerankerStatus === 'unavailable' && { rerankerStatus: 'unavailable' })
      };
    
      type SearchResponsePayload = {
        status: 'success';
        searchQuality: typeof searchQualityBlock & {
          tokenEstimate?: number;
          warning?: string;
        };
        budget: { mode: 'compact' | 'full'; resultCount: number };
        preflight?: typeof preflightPayload;
        patternSummary?: string;
        bestExample?: string;
        nextHops?: Array<{ tool: string; why: string; args?: Record<string, string> }>;
        results: Array<Record<string, unknown>>;
        totalResults?: number;
        relatedMemories?: string[];
      };
    
      // Compact mode (default): bounded response with light graph context
      const isCompact = mode !== 'full';
    
      if (isCompact) {
        const compactResults = results.slice(0, 6);
        const strongMemories = filterStrongMemories(relatedMemories, queryStr);
        const patternSummary = buildPatternSummary();
        const bestExample = getBestExample(compactResults);
        const nextHops = buildNextHops(compactResults, searchQuality);
        const payloadText = finalizeSearchPayloadText(
          {
            status: 'success',
            searchQuality: searchQualityBlock,
            budget: { mode: 'compact', resultCount: compactResults.length },
            ...(preflightPayload && { preflight: preflightPayload }),
            ...(patternSummary && { patternSummary }),
            ...(bestExample && { bestExample }),
            ...(nextHops.length > 0 && { nextHops }),
            results: compactResults.map((r) => {
              const importedByCount = getImportedByCount(r);
              const topExports = getTopExports(r.filePath);
              const scope = buildScopeHeader(r.metadata);
              const health = getResultHealth(r.filePath);
              // First 3 lines of chunk content as a lightweight signature preview
              const signaturePreview = r.snippet
                ? r.snippet
                    .replace(/^\r?\n+/, '')
                    .split('\n')
                    .slice(0, 3)
                    .join('\n')
                    .trim() || undefined
                : undefined;
              return {
                file: `${r.filePath}:${r.startLine}-${r.endLine}`,
                summary: r.summary,
                score: Math.round(r.score * 100) / 100,
                ...(r.relevanceReason && { relevanceReason: r.relevanceReason }),
                ...(r.componentType &&
                  r.layer &&
                  r.layer !== 'unknown' && { type: `${r.componentType}:${r.layer}` }),
                ...(r.trend && r.trend !== 'Stable' && { trend: r.trend }),
                ...(r.patternWarning && { patternWarning: r.patternWarning }),
                importedByCount,
                ...(topExports.length > 0 && { topExports }),
                ...(r.layer && r.layer !== 'unknown' && { layer: r.layer }),
                // Structural metadata: surface AST intelligence already computed at index time
                ...(r.metadata?.symbolName && { symbol: r.metadata.symbolName }),
                ...(r.metadata?.symbolKind && { symbolKind: r.metadata.symbolKind }),
                ...(scope && { scope }),
                ...(health && { health }),
                ...(signaturePreview && { signaturePreview })
              };
            }),
            ...(strongMemories.length > 0 && {
              relatedMemories: strongMemories.map((m) => formatMemoryForOutput(m))
            })
          },
          { mode: 'compact', pretty: true, transportAware: true }
        );
    
        return {
          content: [
            {
              type: 'text',
              text: payloadText
            }
          ]
        };
      }
    
      // Full mode: today's response shape + budget + relevanceReason; consumers removed
      const payloadText = finalizeSearchPayloadText(
        {
          status: 'success',
          searchQuality: searchQualityBlock,
          budget: { mode: 'full', resultCount: results.length },
          ...(preflightPayload && { preflight: preflightPayload }),
          results: results.map((r) => {
            const relationshipsAndHints = buildRelationshipHints(r);
            const enrichedSnippet = includeSnippets
              ? enrichSnippetWithScope(r.snippet, r.metadata, r.filePath, r.startLine)
              : undefined;
            const scope = buildScopeHeader(r.metadata);
            const health = getResultHealth(r.filePath);
            // Chunk-level imports/exports (top 5 each) + complexity
            const chunkImports = r.imports?.slice(0, 5);
            const chunkExports = r.exports?.slice(0, 5);
    
            return {
              file: `${r.filePath}:${r.startLine}-${r.endLine}`,
              summary: r.summary,
              score: Math.round(r.score * 100) / 100,
              ...(r.relevanceReason && { relevanceReason: r.relevanceReason }),
              ...(r.componentType &&
                r.layer &&
                r.layer !== 'unknown' && { type: `${r.componentType}:${r.layer}` }),
              ...(r.trend && r.trend !== 'Stable' && { trend: r.trend }),
              ...(r.patternWarning && { patternWarning: r.patternWarning }),
              ...(relationshipsAndHints.relationships && {
                relationships: relationshipsAndHints.relationships
              }),
              ...(relationshipsAndHints.hints && { hints: relationshipsAndHints.hints }),
              ...(enrichedSnippet && { snippet: enrichedSnippet }),
              // Structural metadata
              ...(r.metadata?.symbolName && { symbol: r.metadata.symbolName }),
              ...(r.metadata?.symbolKind && { symbolKind: r.metadata.symbolKind }),
              ...(scope && { scope }),
              ...(chunkImports && chunkImports.length > 0 && { imports: chunkImports }),
              ...(chunkExports && chunkExports.length > 0 && { exports: chunkExports }),
              ...(health && { health }),
              ...(r.metadata?.cyclomaticComplexity && {
                complexity: r.metadata.cyclomaticComplexity
              })
            };
          }),
          totalResults: results.length,
          ...(relatedMemories.length > 0 && {
            relatedMemories: relatedMemories.slice(0, 3).map((m) => formatMemoryForOutput(m))
          })
        },
        { mode: 'full', pretty: true, transportAware: true }
      );
    
      return {
        content: [
          {
            type: 'text',
            text: payloadText
          }
        ]
      };
    }
  • The `definition` export declaring the tool name ('search_codebase'), description, and JSON Schema input validation (query required, optional mode, intent, limit, includeSnippets, filters with framework/language/componentType/layer/tags).
    export const definition: Tool = {
      name: 'search_codebase',
      description:
        'Search the indexed codebase. Default compact mode returns at most 6 ranked results with ' +
        'light graph context (importedByCount, topExports, layer), a patternSummary, bestExample, ' +
        'nextHops, and response-budget metadata. Use mode="full" for today\'s richer response with ' +
        'full hints arrays and all memories — identical shape as before this parameter existed. ' +
        'IMPORTANT: Pass the intent="edit"|"refactor"|"migrate" to get preflight: edit readiness check with evidence gating.',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'Natural language search query'
          },
          mode: {
            type: 'string',
            enum: ['compact', 'full'],
            description:
              'Response mode. compact (default): max 6 results with light graph context, pattern summary, ' +
              "best example, next hops, and budget metadata. full: today's richer shape + budget metadata.",
            default: 'compact'
          },
          intent: {
            type: 'string',
            enum: ['explore', 'edit', 'refactor', 'migrate'],
            description:
              'Optional. Use "edit", "refactor", or "migrate" to get the full preflight card before making changes.'
          },
          limit: {
            type: 'number',
            description: 'Maximum number of results to return (default: 5)',
            default: 5
          },
          includeSnippets: {
            type: 'boolean',
            description:
              'Include code snippets in results (default: false). If you need code, prefer read_file instead.',
            default: false
          },
          filters: {
            type: 'object',
            description: 'Optional filters',
            properties: {
              framework: {
                type: 'string',
                description: 'Filter by framework (angular, react, nextjs, vue)'
              },
              language: {
                type: 'string',
                description: 'Filter by programming language'
              },
              componentType: {
                type: 'string',
                description: 'Filter by component type (component, service, directive, etc.)'
              },
              layer: {
                type: 'string',
                description:
                  'Filter by architectural layer (presentation, business, data, state, core, shared)'
              },
              tags: {
                type: 'array',
                items: { type: 'string' },
                description: 'Filter by tags'
              }
            }
          }
        },
        required: ['query']
      }
    };
  • Registration in the TOOLS array (line 55-57) as d1 (from search-codebase.ts), and dispatch in dispatchTool() switch case (lines 65-66) mapping 'search_codebase' to h1.
    export const TOOLS: Tool[] = [d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11].map(
      withProjectSelector
    );
    
    export async function dispatchTool(
      name: string,
      args: Record<string, unknown>,
      ctx: ToolContext
    ): Promise<ToolResponse> {
      switch (name) {
        case 'search_codebase':
          return h1(args, ctx);
        case 'get_codebase_metadata':
          return h2(args, ctx);
        case 'get_indexing_status':
          return h3(args, ctx);
        case 'refresh_index':
          return h4(args, ctx);
        case 'get_style_guide':
          return h5(args, ctx);
        case 'get_team_patterns':
          return h6(args, ctx);
        case 'get_symbol_references':
          return h7(args, ctx);
        case 'detect_circular_dependencies':
          return h8(args, ctx);
        case 'remember':
          return h9(args, ctx);
        case 'get_memory':
          return h10(args, ctx);
        case 'get_codebase_health':
          return h11(args, ctx);
        default:
          return {
            content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
            isError: true
          };
      }
    }
  • src/tools/index.ts:5-6 (registration)
    Import of definition as d1 and handle as h1 from './search-codebase.js', which registers the tool in the central tool index.
    import { definition as d1, handle as h1 } from './search-codebase.js';
    import { definition as d2, handle as h2 } from './get-codebase-metadata.js';
  • Usage of 'search_codebase' as a suggested next-step tool in codebase-map.ts fallback exploration logic.
    if (calls.length < 3 && examples.length > 0) {
      const topFile = examples[0].file;
      const query = path.basename(topFile, path.extname(topFile));
      calls.push({
        tool: 'search_codebase',
        args: { query },
        why: 'Explore the top-rated example'
      });
    }
    
    // Priority 4: fallback (always add if under cap)
    if (calls.length < 3) {
      calls.push({
        tool: 'search_codebase',
        args: { query: 'project architecture' },
        why: 'Explore the codebase'
      });
    }
Behavior5/5

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

With no annotations, the description fully carries behavioral transparency. It describes default behavior (compact mode, 6 results, graph context), full mode, intent parameter effects, and hints about response metadata. It also warns about includeSnippets default and recommends read_file for code.

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, front-loading key information about routing and search function. It provides detailed mode and intent guidance but could be more concise by merging some sentences. Overall, it balances detail and readability.

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 no output schema, the description explains the compact mode return shape (graph context, pattern summary, etc.) and mentions budget metadata. It covers filters and parameters. However, it omits error handling, pagination beyond limit, and behavior for empty queries. Still fairly complete for a search tool.

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

Parameters4/5

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

Schema coverage is 100%, baseline is 3. The description adds value by explaining the compact mode result count (at most 6 vs schema default limit of 5), clarifying the intent parameter's role for preflight, and noting the routing behavior for project. This exceeds basic schema info.

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 it searches the indexed codebase, auto-routes to active project, and details the return format. It distinguishes from siblings like get_codebase_health or detect_circular_dependencies by being a search tool.

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

Usage Guidelines4/5

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

The description guides when to use compact vs full mode and when to pass intent for preflight. It notes that includeSnippets is false by default and to prefer read_file. However, it does not contrast with sibling tools or mention when not to use the tool.

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

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/PatrickSys/codebase-context'

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