UNPKG

socraticode

Version:

SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.

180 lines • 9.77 kB
// SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Giancarlo Erra - Altaire Limited import path from "node:path"; import { contextCollectionName, projectIdFromPath } from "../config.js"; import { SEARCH_MIN_SCORE } from "../constants.js"; import { ensureArtifactsIndexed, indexAllArtifacts, loadConfig, removeAllArtifacts, searchArtifacts, } from "../services/context-artifacts.js"; import { ensureQdrantReady } from "../services/docker.js"; import { isIndexingInProgress } from "../services/indexer.js"; import { logger } from "../services/logger.js"; import { getCollectionInfo, loadContextMetadata } from "../services/qdrant.js"; export async function handleContextTool(name, args) { const projectPath = args.projectPath || process.cwd(); const resolvedPath = path.resolve(projectPath); switch (name) { case "codebase_context": { // List all artifacts: from config + their index status const config = await loadConfig(resolvedPath); if (!config?.artifacts?.length) { return [ `No context artifacts configured for: ${resolvedPath}`, "", "To add context artifacts, create a .socraticodecontextartifacts.json file in your project root:", "", "{", ' "artifacts": [', " {", ' "name": "database-schema",', ' "path": "./docs/schema.sql",', ' "description": "PostgreSQL schema — all tables, indexes, constraints, foreign keys."', " }", " ]", "}", "", "Artifacts can be any text file: SQL schemas, API specs (OpenAPI/Protobuf),", "Terraform configs, Kubernetes manifests, architecture docs, etc.", "Directories are also supported — all files within will be indexed.", ].join("\n"); } const projectId = projectIdFromPath(resolvedPath); const collection = contextCollectionName(projectId); const existingStates = await loadContextMetadata(collection); const stateMap = new Map(existingStates?.map((s) => [s.name, s]) ?? []); const lines = [ `Context Artifacts for: ${resolvedPath}`, `Config: .socraticodecontextartifacts.json (${config.artifacts.length} artifact${config.artifacts.length === 1 ? "" : "s"})`, "", ]; for (const artifact of config.artifacts) { const state = stateMap.get(artifact.name); const status = state ? `✓ indexed (${state.chunksIndexed} chunks, ${state.lastIndexedAt})` : "○ not yet indexed"; lines.push(`━━━ ${artifact.name} ━━━`); lines.push(` Path: ${artifact.path}`); lines.push(` Description: ${artifact.description}`); lines.push(` Status: ${status}`); lines.push(""); } lines.push("Use codebase_context_search to search across artifacts.", "Use codebase_context_index to index/re-index all artifacts.", "Use codebase_context_remove to remove all indexed artifacts."); return lines.join("\n"); } case "codebase_context_index": { await ensureQdrantReady(); const config = await loadConfig(resolvedPath); if (!config?.artifacts?.length) { return `No artifacts defined in .socraticodecontextartifacts.json at ${resolvedPath}`; } logger.info("Indexing context artifacts", { projectPath: resolvedPath }); const { indexed, errors } = await indexAllArtifacts(resolvedPath); const lines = [`Context Artifacts — Indexing Complete`, ""]; if (indexed.length > 0) { lines.push(`✓ Indexed ${indexed.length} artifact${indexed.length === 1 ? "" : "s"}:`); for (const a of indexed) { lines.push(` • ${a.name} — ${a.chunksIndexed} chunks`); } lines.push(""); } if (errors.length > 0) { lines.push(`✗ ${errors.length} error${errors.length === 1 ? "" : "s"}:`); for (const e of errors) { lines.push(` • ${e.name}: ${e.error}`); } lines.push(""); } lines.push("Artifacts are now searchable via codebase_context_search."); return lines.join("\n"); } case "codebase_context_search": { await ensureQdrantReady(); const query = args.query; if (!query?.trim()) { return "Error: query parameter is required for codebase_context_search."; } const artifactName = args.artifactName; const limit = args.limit || 10; // Auto-index: ensure artifacts are indexed and up to date const config = await loadConfig(resolvedPath); if (!config?.artifacts?.length) { return [ `No context artifacts configured for: ${resolvedPath}`, "", "Create a .socraticodecontextartifacts.json file to define artifacts.", "Then use codebase_context_search to search them.", ].join("\n"); } // Check if collection exists; if not, do full index const projectId = projectIdFromPath(resolvedPath); const collection = contextCollectionName(projectId); const collInfo = await getCollectionInfo(collection); if (!collInfo || collInfo.pointsCount === 0) { // First time — do full index logger.info("Auto-indexing context artifacts (first search)", { projectPath: resolvedPath }); const { indexed, errors } = await indexAllArtifacts(resolvedPath); if (indexed.length === 0 && errors.length > 0) { return `Failed to index artifacts:\n${errors.map((e) => ` • ${e.name}: ${e.error}`).join("\n")}`; } } else { // Check for staleness and re-index changed artifacts const { reindexed, errors } = await ensureArtifactsIndexed(resolvedPath); if (reindexed.length > 0) { logger.info("Auto-re-indexed stale artifacts", { reindexed, projectPath: resolvedPath, }); } if (errors.length > 0) { logger.warn("Some artifacts failed staleness check", { errors }); } } // Search const allResults = await searchArtifacts(resolvedPath, query, artifactName, limit); // Apply minimum score threshold const minScore = args.minScore ?? SEARCH_MIN_SCORE; const results = minScore > 0 ? allResults.filter((r) => r.score >= minScore) : allResults; const filteredCount = allResults.length - results.length; if (results.length === 0) { const filterNote = artifactName ? ` in artifact "${artifactName}"` : ""; if (filteredCount > 0) { return `No results above score threshold ${minScore.toFixed(2)} for "${query}"${filterNote} in context artifacts of ${resolvedPath}.\n${filteredCount} result${filteredCount === 1 ? " was" : "s were"} below the threshold. Try a broader query or lower the minScore parameter.`; } return `No results found for "${query}"${filterNote} in context artifacts of ${resolvedPath}.\nCheck that your .socraticodecontextartifacts.json defines the relevant artifacts.`; } const lines = [ `Context search results for "${query}" (${results.length} match${results.length === 1 ? "" : "es"}):`, "", ]; for (const r of results) { const artifactLabel = r.artifactName ?? r.language; lines.push(`--- ${r.relativePath} [${artifactLabel}] (lines ${r.startLine}-${r.endLine}) score: ${r.score.toFixed(4)} ---`); lines.push(r.content); lines.push(""); } if (filteredCount > 0) { lines.push(`(${filteredCount} additional result${filteredCount === 1 ? "" : "s"} below score threshold ${minScore.toFixed(2)} omitted)`); } return lines.join("\n"); } case "codebase_context_remove": { // Guard: if indexing/update is in progress it includes context artifact indexing if (isIndexingInProgress(resolvedPath)) { return [ `⚠ Cannot remove context artifacts: indexing is in progress for ${resolvedPath}`, "", "The current indexing/update operation includes context artifact indexing.", "Please wait for it to finish (use codebase_status) or stop it with codebase_stop first.", ].join("\n"); } await ensureQdrantReady(); await removeAllArtifacts(resolvedPath); return `Removed all indexed context artifacts for: ${resolvedPath}`; } default: return `Unknown context tool: ${name}`; } } //# sourceMappingURL=context-tools.js.map