Skip to content

Commit 1e24175

Browse files
feat(mcp): add retrieve docs explain diagnostics
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 2218950 commit 1e24175

3 files changed

Lines changed: 65 additions & 1 deletion

File tree

internal/mcp/handler_docs.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ func (h *handlers) retrieveDocs(ctx context.Context, request mcp.CallToolRequest
331331
if contentLimit > 20000 {
332332
return mcp.NewToolResultError("content_limit must be <= 20000"), nil
333333
}
334+
explain := request.GetBool("explain", false)
334335

335336
namespace := requestNamespace(request)
336337
indexPath, err := h.resolvedRagIndexPath(namespace)
@@ -349,14 +350,15 @@ func (h *handlers) retrieveDocs(ctx context.Context, request mcp.CallToolRequest
349350
"content_limit": contentLimit,
350351
"namespace": namespace,
351352
"mtime": indexMtime,
353+
"explain": explain,
352354
}
353355
return finalizeToolResult(h.cachedExecute(ctx, "retrieve_docs:", cacheKey, func() (string, error) {
354356
idx, err := ragindex.LoadIndex(indexPath)
355357
if err != nil {
356358
return "", newToolResultErr(fmt.Sprintf("load doc-index: %v", err))
357359
}
358360

359-
candidates := ragindex.Retrieve(idx.Root, query, pageReq.Limit)
361+
candidates := ragindex.RetrieveWithOptions(idx.Root, query, pageReq.Limit, ragindex.RetrieveOptions{Explain: explain})
360362
response := retrieveDocsResponse{Results: make([]retrieveDocsResult, 0, len(candidates))}
361363
for _, candidate := range candidates {
362364
result := retrieveDocsResult{RetrieveResult: candidate}

internal/mcp/handler_docs_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,67 @@ func TestRetrieveDocs_ExposesStructuredMatchedFields(t *testing.T) {
445445
}
446446
}
447447

448+
func TestRetrieveDocs_ExplainFlagControlsDiagnostics(t *testing.T) {
449+
deps := setupTestDeps(t)
450+
tmpDir := t.TempDir()
451+
docsDir := filepath.Join(tmpDir, "docs")
452+
deps.RagIndexDir = filepath.Join(tmpDir, ".ccg")
453+
454+
comm := model.Community{Key: "billing", Label: "Billing", Description: "billing pipeline"}
455+
if err := deps.DB.Create(&comm).Error; err != nil {
456+
t.Fatalf("create community: %v", err)
457+
}
458+
node := model.Node{QualifiedName: "billing.PaymentProcessor", Kind: model.NodeKindFunction, Name: "PaymentProcessor", FilePath: "internal/billing/processor.go", StartLine: 1, EndLine: 30, Language: "go"}
459+
if err := deps.DB.Create(&node).Error; err != nil {
460+
t.Fatalf("create node: %v", err)
461+
}
462+
if err := deps.DB.Create(&model.CommunityMembership{CommunityID: comm.ID, NodeID: node.ID}).Error; err != nil {
463+
t.Fatalf("create membership: %v", err)
464+
}
465+
ann := model.Annotation{NodeID: node.ID, Summary: "payment processor entrypoint"}
466+
if err := deps.DB.Create(&ann).Error; err != nil {
467+
t.Fatalf("create annotation: %v", err)
468+
}
469+
if err := deps.DB.Create(&model.DocTag{AnnotationID: ann.ID, Kind: model.TagIntent, Value: "payment settlement entrypoint", Ordinal: 0}).Error; err != nil {
470+
t.Fatalf("create intent tag: %v", err)
471+
}
472+
473+
docPath := filepath.Join(docsDir, "internal/billing/processor.go.md")
474+
if err := os.MkdirAll(filepath.Dir(docPath), 0o755); err != nil {
475+
t.Fatal(err)
476+
}
477+
if err := os.WriteFile(docPath, []byte("# processor.go\n\npayment processor docs\n"), 0o644); err != nil {
478+
t.Fatal(err)
479+
}
480+
481+
b := &ragindex.Builder{DB: deps.DB, IndexDir: deps.RagIndexDir, OutDir: docsDir}
482+
if _, _, err := b.Build(context.Background()); err != nil {
483+
t.Fatalf("Build: %v", err)
484+
}
485+
486+
defaultRes := callTool(t, deps, "retrieve_docs", map[string]any{"query": "payment", "limit": float64(3), "content_limit": float64(0)})
487+
if defaultRes.IsError {
488+
t.Fatalf("retrieve_docs default error: %v", getTextContent(defaultRes))
489+
}
490+
defaultJSON := getTextContent(defaultRes)
491+
for _, key := range []string{"expanded_terms", "field_scores", "literal_score", "expansion_score"} {
492+
if strings.Contains(defaultJSON, key) {
493+
t.Fatalf("default response must omit %q diagnostic key, got %s", key, defaultJSON)
494+
}
495+
}
496+
497+
explainRes := callTool(t, deps, "retrieve_docs", map[string]any{"query": "payment", "limit": float64(3), "content_limit": float64(0), "explain": true})
498+
if explainRes.IsError {
499+
t.Fatalf("retrieve_docs explain error: %v", getTextContent(explainRes))
500+
}
501+
explainJSON := getTextContent(explainRes)
502+
for _, key := range []string{"field_scores", "literal_score"} {
503+
if !strings.Contains(explainJSON, key) {
504+
t.Fatalf("explain response must include %q, got %s", key, explainJSON)
505+
}
506+
}
507+
}
508+
448509
func TestRetrieveDocs_ContentLimitZeroOmitsContent(t *testing.T) {
449510
deps := setupTestDeps(t)
450511
tmpDir := t.TempDir()

internal/mcp/tools_docs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func docsTools(h *handlers) []server.ServerTool {
5353
mcp.WithNumber("limit", mcp.Description("Maximum number of document results (default: 5, max: 50)")),
5454
mcp.WithNumber("content_limit", mcp.Description("Maximum bytes of Markdown content per result (default: 4000, max: 20000; use 0 to omit content)")),
5555
mcp.WithString("namespace", mcp.Description("Namespace. When set, retrieves from the namespace-specific doc-index.json.")),
56+
mcp.WithBoolean("explain", mcp.Description("When true, include per-result expanded_terms, field_scores, literal_score, and expansion_score diagnostics (default: false).")),
5657
),
5758
Handler: h.retrieveDocs,
5859
},

0 commit comments

Comments
 (0)