Skip to content

Commit ee6a1b9

Browse files
committed
Remove fallback to doc/wiki index snapshots for doc APIs
1 parent 75d9cd3 commit ee6a1b9

4 files changed

Lines changed: 173 additions & 399 deletions

File tree

internal/mcp/handler_docs.go

Lines changed: 36 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ func (h *handlers) buildRagIndex(ctx context.Context, request mcp.CallToolReques
148148
}
149149

150150
// getRagTree returns the documentation tree or a pruned node subtree.
151-
// @intent Exposes the documentation RAG index in a tree format to enable exploratory lookups, falling back to DB synthesis when doc-index.json is absent.
151+
// @intent Exposes the documentation RAG index in a tree format from DB graph evidence.
152152
// @param request node_id is the starting point for a subtree, community_id is a deprecated alias, and depth limits the return depth.
153-
// @ensures Returns the TreeNode JSON for the requested range on success and prefers doc-index.json when present.
153+
// @ensures Returns the TreeNode JSON for the requested range on success.
154154
// @see mcp.handlers.buildRagIndex
155155
func (h *handlers) getRagTree(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
156156
nodeID := request.GetString("node_id", "")
@@ -160,66 +160,32 @@ func (h *handlers) getRagTree(ctx context.Context, request mcp.CallToolRequest)
160160
depth := int(request.GetFloat("depth", 0))
161161
namespace := resolveNamespace(ctx, requestNamespace(request))
162162
ctx = ctxns.WithNamespace(ctx, namespace)
163-
indexNamespace := namespace
164-
if namespace == ctxns.DefaultNamespace {
165-
indexNamespace = ""
166-
}
167-
indexPath, err := h.resolvedRagIndexPath(indexNamespace)
168-
if err != nil {
169-
return mcp.NewToolResultError(err.Error()), nil
163+
if h.deps.DB == nil {
164+
return mcp.NewToolResultError("DB is not configured"), nil
170165
}
171-
172-
stat, statErr := os.Stat(indexPath)
173-
if statErr != nil {
174-
if !os.IsNotExist(statErr) {
175-
return mcp.NewToolResultError(fmt.Sprintf("stat doc-index: %v", statErr)), nil
166+
return finalizeToolResult(h.cachedExecute(ctx, "get_rag_tree:db:", map[string]any{"node_id": nodeID, "depth": depth, "namespace": namespace}, func() (string, error) {
167+
builder := &ragindex.Builder{
168+
DB: h.deps.DB,
169+
OutDir: "docs",
170+
ProjectDesc: h.deps.RagProjectDesc,
176171
}
177-
return finalizeToolResult(h.cachedExecute(ctx, "get_rag_tree:db:", map[string]any{"node_id": nodeID, "depth": depth, "namespace": namespace}, func() (string, error) {
178-
if h.deps.DB == nil {
179-
return "", newToolResultErr("DB not configured")
180-
}
181-
builder := &ragindex.Builder{
182-
DB: h.deps.DB,
183-
OutDir: "docs",
184-
ProjectDesc: h.deps.RagProjectDesc,
185-
}
186-
root, _, _, err := builder.BuildTree(ctx)
187-
if err != nil {
188-
return "", newToolResultErr(fmt.Sprintf("build rag tree from DB: %v", err))
189-
}
190-
node, err := selectRagTreeNode(root, nodeID)
191-
if err != nil {
192-
return "", err
193-
}
194-
node = ragindex.PruneTree(node, depth)
195-
196-
// TreeNode fields are JSON-safe DTO fields; json.Marshal cannot fail for this payload.
197-
encoded, _ := json.Marshal(node)
198-
return string(encoded), nil
199-
}))
200-
}
201-
202-
indexMtime := stat.ModTime().UnixNano()
203-
return finalizeToolResult(h.cachedExecute(ctx, "get_rag_tree:", map[string]any{"node_id": nodeID, "depth": depth, "namespace": namespace, "mtime": indexMtime}, func() (string, error) {
204-
idx, err := ragindex.LoadIndex(indexPath)
172+
root, _, _, err := builder.BuildTree(ctx)
205173
if err != nil {
206-
return "", newToolResultErr(fmt.Sprintf("load doc-index: %v", err))
174+
return "", newToolResultErr(fmt.Sprintf("build rag tree from DB: %v", err))
207175
}
208-
209-
node, err := selectRagTreeNode(idx.Root, nodeID)
176+
node, err := selectRagTreeNode(root, nodeID)
210177
if err != nil {
211178
return "", err
212179
}
213-
214180
node = ragindex.PruneTree(node, depth)
215181

216182
// TreeNode fields are JSON-safe DTO fields; json.Marshal cannot fail for this payload.
217-
b, _ := json.Marshal(node)
218-
return string(b), nil
183+
encoded, _ := json.Marshal(node)
184+
return string(encoded), nil
219185
}))
220186
}
221187

222-
// @intent select the requested RAG tree node with shared not-found semantics for JSON indexes and DB fallback trees.
188+
// @intent select the requested RAG tree node in DB-built trees.
223189
func selectRagTreeNode(root *ragindex.TreeNode, nodeID string) (*ragindex.TreeNode, error) {
224190
if nodeID == "" {
225191
return root, nil
@@ -305,45 +271,20 @@ func (h *handlers) searchDocs(ctx context.Context, request mcp.CallToolRequest)
305271
}
306272
namespace := requestNamespace(request)
307273
ctx = h.applyNamespace(ctx, request)
308-
indexPath, err := h.resolvedRagIndexPath(namespace)
309-
if err != nil {
310-
return mcp.NewToolResultError(err.Error()), nil
311-
}
312-
313-
stat, statErr := os.Stat(indexPath)
314-
if statErr != nil && !os.IsNotExist(statErr) {
315-
return mcp.NewToolResultError(fmt.Sprintf("stat doc-index: %v", statErr)), nil
316-
}
317-
if statErr != nil && os.IsNotExist(statErr) {
318-
return finalizeToolResult(h.cachedExecute(ctx, "search_docs:db:", map[string]any{"query": query, "limit": pageReq.Limit, "namespace": namespace}, func() (string, error) {
319-
results, err := h.searchDocsFromDB(ctx, namespace, query, pageReq.Limit)
320-
if err != nil {
321-
return "", newToolResultErr(err.Error())
322-
}
323-
b, _ := json.Marshal(results)
324-
return string(b), nil
325-
}))
274+
if h.deps.DB == nil {
275+
return mcp.NewToolResultError("DB is not configured"), nil
326276
}
327-
indexMtime := stat.ModTime().UnixNano()
328-
329-
return finalizeToolResult(h.cachedExecute(ctx, "search_docs:", map[string]any{"query": query, "limit": pageReq.Limit, "namespace": namespace, "mtime": indexMtime}, func() (string, error) {
330-
idx, err := ragindex.LoadIndex(indexPath)
277+
return finalizeToolResult(h.cachedExecute(ctx, "search_docs:db:", map[string]any{"query": query, "limit": pageReq.Limit, "namespace": namespace}, func() (string, error) {
278+
results, err := h.searchDocsFromDB(ctx, namespace, query, pageReq.Limit)
331279
if err != nil {
332-
return "", newToolResultErr(fmt.Sprintf("load doc-index: %v", err))
280+
return "", newToolResultErr(err.Error())
333281
}
334-
335-
results := ragindex.Search(idx.Root, query, pageReq.Limit)
336-
if results == nil {
337-
results = []ragindex.SearchResult{}
338-
}
339-
340-
// SearchResult fields are all basic types (string, []string); json.Marshal cannot fail.
341282
b, _ := json.Marshal(results)
342283
return string(b), nil
343284
}))
344285
}
345286

346-
// @intent search persisted graph nodes when search_docs has no generated doc-index.json to load.
287+
// @intent search persisted graph nodes directly from DB and search backend.
347288
// @requires ctx must carry the selected namespace for SearchBackend.Query.
348289
// @ensures returns SearchResult-compatible JSON items without requiring generated index files.
349290
// @sideEffect queries the search backend and may scan graph annotations as a fallback.
@@ -387,7 +328,7 @@ type retrieveDocsResult = retrieval.Result
387328

388329
var _ = (*handlers).retrieveDocsFromDB
389330

390-
// @intent build a retrieve_docs response from persisted graph nodes and annotation tags before consulting compatibility doc-index snapshots.
331+
// @intent build a retrieve_docs response from persisted graph nodes and annotation tags.
391332
// @requires SearchBackend and DB must be configured, and ctx must carry the desired namespace.
392333
// @ensures returned results are grouped one-per-file in first-seen FTS order and keep retrieve_docs' stable JSON shape.
393334
// @sideEffect queries the search backend and may read generated Markdown files for bounded content.
@@ -402,12 +343,12 @@ func (h *handlers) retrieveDocsFromDB(ctx context.Context, namespace, query stri
402343
return retrieved, nil
403344
}
404345

405-
// retrieveDocs retrieves generated Markdown docs using DB-backed graph evidence first and doc-index snapshots as fallback.
406-
// @intent provide DB-primary document retrieval without replacing quick keyword search or removing doc-index compatibility.
346+
// retrieveDocs retrieves generated Markdown docs using DB-backed graph evidence.
347+
// @intent provide DB-primary document retrieval.
407348
// @param request query is the natural-language retrieval prompt, limit bounds document count, and content_limit bounds each Markdown payload.
408-
// @requires either a configured DB or doc-index.json and generated docs must exist.
349+
// @requires a configured DB and generated docs may exist.
409350
// @ensures returns file-level matches with tree evidence and bounded document content.
410-
// @sideEffect queries the graph DB and may read doc-index.json and generated Markdown files.
351+
// @sideEffect queries the graph DB and reads generated Markdown files.
411352
func (h *handlers) retrieveDocs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
412353
query, err := request.RequireString("query")
413354
if err != nil {
@@ -434,88 +375,28 @@ func (h *handlers) retrieveDocs(ctx context.Context, request mcp.CallToolRequest
434375

435376
namespace := requestNamespace(request)
436377
ctx = h.applyNamespace(ctx, request)
437-
indexPath, err := h.resolvedRagIndexPath(namespace)
438-
if err != nil {
439-
return mcp.NewToolResultError(err.Error()), nil
440-
}
441-
442-
var dbErr error
443-
if h.deps.DB != nil {
444-
cacheKey := map[string]any{
445-
"query": query,
446-
"limit": pageReq.Limit,
447-
"content_limit": contentLimit,
448-
"namespace": namespace,
449-
"explain": explain,
450-
}
451-
result, err := h.cachedExecute(ctx, "retrieve_docs:db:", cacheKey, func() (string, error) {
452-
response, err := h.retrieveDocsFromDB(ctx, namespace, query, pageReq.Limit, contentLimit, explain)
453-
if err != nil {
454-
return "", newToolResultErr(err.Error())
455-
}
456-
457-
b, _ := json.Marshal(response)
458-
return string(b), nil
459-
})
460-
if err == nil {
461-
return finalizeToolResult(result, nil)
462-
}
463-
dbErr = err
464-
}
465-
466-
stat, statErr := os.Stat(indexPath)
467-
if statErr != nil && !os.IsNotExist(statErr) {
468-
return mcp.NewToolResultError(fmt.Sprintf("stat doc-index: %v", statErr)), nil
469-
}
470-
if statErr != nil && os.IsNotExist(statErr) {
471-
if dbErr != nil {
472-
return finalizeToolResult("", dbErr)
473-
}
474-
return finalizeToolResult("", newToolResultErr(fmt.Sprintf("load doc-index: %v", statErr)))
378+
if h.deps.DB == nil {
379+
return mcp.NewToolResultError("DB is not configured"), nil
475380
}
476-
indexMtime := stat.ModTime().UnixNano()
477-
478381
cacheKey := map[string]any{
479382
"query": query,
480383
"limit": pageReq.Limit,
481384
"content_limit": contentLimit,
482385
"namespace": namespace,
483-
"mtime": indexMtime,
484386
"explain": explain,
485387
}
486-
return finalizeToolResult(h.cachedExecute(ctx, "retrieve_docs:", cacheKey, func() (string, error) {
487-
return h.retrieveDocsFromIndex(namespace, query, pageReq.Limit, contentLimit, explain, indexPath)
488-
}))
489-
}
490-
491-
// @intent run compatibility retrieve_docs against a generated doc-index.json snapshot when DB retrieval is unavailable.
492-
// @sideEffect reads doc-index.json and optionally generated Markdown content.
493-
func (h *handlers) retrieveDocsFromIndex(namespace, query string, limit, contentLimit int, explain bool, indexPath string) (string, error) {
494-
idx, err := ragindex.LoadIndex(indexPath)
495-
if err != nil {
496-
return "", newToolResultErr(fmt.Sprintf("load doc-index: %v", err))
497-
}
498-
499-
candidates := ragindex.RetrieveWithOptions(idx.Root, query, limit, ragindex.RetrieveOptions{Explain: explain})
500-
response := retrieveDocsResponse{Results: make([]retrieveDocsResult, 0, len(candidates))}
501-
for _, candidate := range candidates {
502-
result := retrieveDocsResult{RetrieveResult: candidate}
503-
if contentLimit > 0 {
504-
content, truncated, err := h.readIndexedDocContent(namespace, candidate.DocPath, contentLimit)
505-
if err != nil {
506-
return "", newToolResultErr(err.Error())
507-
}
508-
result.Content = content
509-
result.ContentTruncated = truncated
388+
return finalizeToolResult(h.cachedExecute(ctx, "retrieve_docs:db:", cacheKey, func() (string, error) {
389+
response, err := h.retrieveDocsFromDB(ctx, namespace, query, pageReq.Limit, contentLimit, explain)
390+
if err != nil {
391+
return "", newToolResultErr(err.Error())
510392
}
511-
response.Results = append(response.Results, result)
512-
}
513393

514-
b, _ := json.Marshal(response)
515-
return string(b), nil
394+
b, _ := json.Marshal(response)
395+
return string(b), nil
396+
}))
516397
}
517398

518-
// readIndexedDocContent reads a doc_path stored in doc-index.json under the docs/index root.
399+
// readIndexedDocContent reads a doc_path under the generated docs root.
519400
// @intent let retrieve_docs return bounded Markdown content while keeping index-provided paths inside a safe root.
520401
// @domainRule namespace doc paths are resolved from that namespace; shared doc paths are resolved from the parent of the RAG index root.
521402
// @sideEffect reads a generated Markdown file.

0 commit comments

Comments
 (0)