/** * MCP gno_get tool - Retrieve single document. * * @module src/mcp/tools/get */ import { join as pathJoin } from "node:path"; import type { DocumentRow, StorePort } from "../../store/types"; import type { ToolContext } from "../server"; import { decorateUriForIndex, parseUri } from "../../app/constants"; import { getDocumentCapabilities, type DocumentCapabilities, } from "../../core/document-capabilities"; import { resolveEffectiveIndex } from "../../core/indexed-reference"; import { projectRecordEvidenceMetadata, type RecordEvidenceMetadata, } from "../../core/record-metadata"; import { parseRef } from "../../core/ref-parser"; import { attachRetrievalTraceMetadata, evidenceFromExactDocument, RetrievalTraceSession, } from "../../core/retrieval-trace-session"; import { openScopedIndexStore } from "../../store/sqlite/scoped-index"; import { runTool, type ToolResult } from "./index"; interface GetInput { ref: string; fromLine?: number; lineCount?: number; lineNumbers?: boolean; traceId?: string; } interface GetResponse { docid: string; uri: string; title?: string; content: string; totalLines: number; returnedLines?: { start: number; end: number }; language?: string; source: { absPath?: string; relPath: string; mime: string; ext: string; modifiedAt?: string; sizeBytes?: number; sourceHash?: string; }; conversion?: { converterId?: string; converterVersion?: string; mirrorHash?: string; }; record?: RecordEvidenceMetadata; capabilities: DocumentCapabilities; } /** * Lookup document by parsed reference. */ async function lookupDocument( store: StorePort, parsed: ReturnType ): Promise { if ("error" in parsed) { return null; } switch (parsed.type) { case "docid": { const result = await store.getDocumentByDocid(parsed.value); return result.ok ? result.value : null; } case "uri": { const result = await store.getDocumentByUri(parsed.value); return result.ok ? result.value : null; } case "collPath": { if (!(parsed.collection && parsed.relPath)) { return null; } const result = await store.getDocument(parsed.collection, parsed.relPath); return result.ok ? result.value : null; } default: return null; } } /** * Format get response as text. */ function formatGetResponse(data: GetResponse): string { const lines: string[] = []; lines.push(`Document: ${data.uri}`); if (data.title) { lines.push(`Title: ${data.title}`); } lines.push(`Lines: ${data.totalLines}`); if (data.source.absPath) { lines.push(`Path: ${data.source.absPath}`); } lines.push(""); if (data.returnedLines) { lines.push( `--- Content (lines ${data.returnedLines.start}-${data.returnedLines.end}) ---` ); } else { lines.push("--- Content ---"); } lines.push(data.content); return lines.join("\n"); } /** * Handle gno_get tool call. */ export function handleGet( args: GetInput, ctx: ToolContext ): Promise { return runTool( ctx, "gno_get", // oxlint-disable-next-line max-lines-per-function -- document retrieval with multiple ref formats async () => { // Parse reference const parsed = parseRef(args.ref); if ("error" in parsed) { throw new Error(parsed.error); } const resolution = resolveEffectiveIndex([args.ref], ctx.indexName); if (!resolution.ok) { throw new Error(resolution.error); } const scoped = await openScopedIndexStore({ activeStore: ctx.store, activeIndexName: ctx.indexName, requestedIndexName: resolution.value.indexName, config: ctx.config, configPath: ctx.actualConfigPath, }); try { // Lookup document const doc = await lookupDocument(scoped.store, parsed); if (!doc) { throw new Error(`Document not found: ${args.ref}`); } // Get content if (!doc.mirrorHash) { throw new Error("Document has no indexed content"); } const contentResult = await scoped.store.getContent(doc.mirrorHash); if (!contentResult.ok) { throw new Error(contentResult.error.message); } const fullContent = contentResult.value ?? ""; const contentLines = fullContent.split("\n"); const totalLines = contentLines.length; // Apply line range if specified let content = fullContent; let exactContent = fullContent; let returnedLines: { start: number; end: number } | undefined; // lineNumbers defaults to true per spec const showLineNumbers = args.lineNumbers !== false; if (args.fromLine || args.lineCount) { const startLine = args.fromLine ?? 1; // Clamp startLine to valid range if (startLine > totalLines) { // Return empty content for out-of-range request content = ""; exactContent = ""; returnedLines = undefined; } else { const count = args.lineCount ?? totalLines - startLine + 1; const endLine = Math.min(startLine + count - 1, totalLines); const slicedLines = contentLines.slice(startLine - 1, endLine); exactContent = slicedLines.join("\n"); if (showLineNumbers) { content = slicedLines .map((line, i) => `${startLine + i}: ${line}`) .join("\n"); } else { content = slicedLines.join("\n"); } returnedLines = { start: startLine, end: endLine }; } } else if (showLineNumbers) { content = contentLines .map((line, i) => `${i + 1}: ${line}`) .join("\n"); } // Build absPath const uriParsed = parseUri(doc.uri); const sourceRelPath = doc.recordSourcePath ?? doc.relPath; let absPath: string | undefined; if (uriParsed) { const collection = ctx.collections.find( (c) => c.name === uriParsed.collection ); if (collection) { absPath = pathJoin(collection.path, sourceRelPath); } } const response: GetResponse = { docid: doc.docid, uri: decorateUriForIndex(doc.uri, scoped.indexName), title: doc.title ?? undefined, content, totalLines, returnedLines, language: doc.languageHint ?? undefined, source: { absPath, relPath: sourceRelPath, mime: doc.sourceMime, ext: doc.sourceExt, modifiedAt: doc.sourceMtime, sizeBytes: doc.sourceSize, sourceHash: doc.sourceHash, }, conversion: doc.mirrorHash ? { converterId: doc.converterId ?? undefined, converterVersion: doc.converterVersion ?? undefined, mirrorHash: doc.mirrorHash, } : undefined, record: projectRecordEvidenceMetadata(doc), capabilities: getDocumentCapabilities({ sourceExt: doc.sourceExt, sourceMime: doc.sourceMime, contentAvailable: doc.mirrorHash !== null, recordKey: doc.recordKey, }), }; if (!args.traceId) return response; const resumed = await RetrievalTraceSession.resume({ store: scoped.store, config: ctx.config.retrievalTraces, traceId: args.traceId, }); if (!resumed.ok) throw new Error(resumed.error.message); const traceSession = resumed.value; if (!traceSession) return response; const lines = returnedLines ?? { start: 1, end: totalLines }; const evidence = evidenceFromExactDocument({ docid: response.docid, uri: response.uri, sourceHash: response.source.sourceHash, mirrorHash: response.conversion?.mirrorHash, content: exactContent, startLine: lines.start, endLine: lines.end, }); if (evidence) { const got = await traceSession.recordEvidence("get", [evidence]); if (!got.ok) throw new Error(got.error.message); const opened = await traceSession.recordEvidence("open", [evidence]); if (!opened.ok) throw new Error(opened.error.message); } return attachRetrievalTraceMetadata(response, traceSession); } finally { await scoped.close(); } }, formatGetResponse ); }