Skip to content

Commit 590809a

Browse files
committed
refactor(mcp): require daemon for search_files and remove internal/search package
1 parent 9b0da8d commit 590809a

File tree

9 files changed

+39
-687
lines changed

9 files changed

+39
-687
lines changed

CLAUDE.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ The following principles guide development decisions for this project:
5656

5757
Files are processed through three distinct phases:
5858

59-
1. **Metadata Extraction** (`internal/metadata/`) - Fast, deterministic extraction using specialized handlers for 9 file type categories
59+
1. **Metadata Extraction** (`internal/metadata/`) - Fast, deterministic extraction using specialized handlers for 8 file type categories (Markdown, Docx, Pptx, PDF, Image, VTT, JSON, Code)
6060
2. **Semantic Analysis** (`internal/semantic/`) - AI-powered content understanding via provider abstraction supporting Claude, OpenAI, and Gemini
6161
3. **Knowledge Graph Storage** (`internal/graph/`) - FalkorDB stores files, tags, topics, entities, and relationships for semantic search
6262

@@ -138,7 +138,7 @@ Detailed technical documentation for each subsystem is available in `docs/subsys
138138
| [integrations](docs/subsystems/integrations/) | Framework-agnostic integration with dual-hook architecture |
139139
| [logging](docs/subsystems/logging/) | Structured logging with slog, rotation, and context propagation |
140140
| [mcp](docs/subsystems/mcp/) | Model Context Protocol with JSON-RPC 2.0 and graph-powered tools |
141-
| [metadata](docs/subsystems/metadata/) | Fast metadata extraction with handlers for 9 file type categories |
141+
| [metadata](docs/subsystems/metadata/) | Fast metadata extraction with handlers for 8 file type categories |
142142
| [semantic](docs/subsystems/semantic/) | Multi-provider AI content understanding with intelligent routing |
143143
| [tui](docs/subsystems/tui/) | Interactive setup wizard built on Bubble Tea |
144144
| [version](docs/subsystems/version/) | Build-time version injection with embedded fallback |
@@ -293,7 +293,9 @@ make build # Build binary with git version info
293293
make install # Install to ~/.local/bin
294294

295295
# Testing
296-
make test # Run all tests
296+
make test # Run unit tests
297+
make test-integration # Run integration tests
298+
make test-all # Run all non-e2e tests (unit + integration)
297299
make test-race # Run tests with race detector
298300
make test-e2e # Run E2E tests
299301
make test-e2e-quick # Run quick E2E smoke tests
@@ -309,6 +311,7 @@ make coverage-html # Generate HTML coverage report
309311
# Cleanup
310312
make clean # Clean build artifacts
311313
make clean-cache # Clean cache files
314+
make uninstall # Remove installed binary
312315
make deps # Download and tidy dependencies
313316

314317
# Daemon development

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ Agentic Memorizer integrates with multiple AI agent frameworks, providing automa
244244
- Up to 50 facts, 10-500 characters each
245245
- Facts injected via UserPromptSubmit (Claude) / BeforeAgent (Gemini) hooks
246246

247-
**Semantic Search** (`internal/search/`):
247+
**Graph Search** (`internal/graph/`):
248248
- Graph-powered Cypher queries
249249
- Full-text search on summaries
250250
- Entity-based file discovery
@@ -2572,7 +2572,6 @@ agentic-memorizer/
25722572
│ ├── metadata/ # File metadata extraction (9 category handlers)
25732573
│ ├── semantic/ # Multi-provider semantic analysis (Claude, OpenAI, Gemini)
25742574
│ ├── cache/ # Content-addressable analysis caching
2575-
│ ├── search/ # Semantic search engine (graph-powered)
25762575
│ ├── format/ # Output formatting system
25772576
│ │ ├── formatters/ # Individual formatters (text, JSON, XML, YAML, markdown)
25782577
│ │ └── testdata/ # Test data for formatters

docs/subsystems/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ Model Context Protocol implementation with JSON-RPC 2.0 messaging, stdio transpo
409409
- Five graph-powered tools: search_files, get_file_metadata, list_recent_files, get_related_files, search_entities
410410
- Three resources: file index in XML, JSON, and Markdown formats with subscription support
411411
- Three built-in prompts: analyze-file, search-context, explain-summary
412-
- Dual-source fallback to in-memory index when daemon unavailable
412+
- Partial fallback for two tools (get_file_metadata, list_recent_files) when daemon unavailable
413413
- Real-time updates via SSE client for subscribed resource notifications
414414

415415
**Primary Components:**
@@ -569,6 +569,7 @@ Comprehensive integration testing with isolated environments, Docker-based Falko
569569

570570
**Recent Updates:**
571571

572+
- Updated mcp subsystem documentation - search_files now requires daemon (2025-12-29)
572573
- Created cli subsystem documentation (2025-12-29)
573574
- Created tui subsystem documentation (2025-12-29)
574575
- Created logging subsystem documentation (2025-12-29)

docs/subsystems/mcp/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Key capabilities include:
2727
- **Five graph tools** - search_files, get_file_metadata, list_recent_files, get_related_files, search_entities
2828
- **Three resources** - File index in XML, JSON, and Markdown formats with subscription support
2929
- **Three prompts** - Built-in prompts for file analysis, search context, and summary explanation
30-
- **Fallback operation** - Graceful degradation to in-memory index when daemon unavailable
30+
- **Partial fallback** - Two tools (get_file_metadata, list_recent_files) degrade to in-memory index when daemon unavailable
3131
- **Real-time updates** - SSE client receives index changes and notifies subscribed clients
3232

3333
## Design Principles
@@ -42,7 +42,7 @@ Tool handlers implement a common Handler interface with Name, Execute, and ToolD
4242

4343
### Dual-Source Fallback Strategy
4444

45-
Three of the five tools (search_files, get_file_metadata, list_recent_files) implement dual-source logic: they first attempt the daemon HTTP API for current graph data, then fall back to the in-memory index if the daemon is unavailable. This enables degraded operation without complete failure. The remaining tools (get_related_files, search_entities) require the daemon's graph database and cannot fall back.
45+
Two of the five tools (get_file_metadata, list_recent_files) implement dual-source logic: they first attempt the daemon HTTP API for current graph data, then fall back to the in-memory index if the daemon is unavailable. This enables degraded operation without complete failure. The remaining three tools (search_files, get_related_files, search_entities) require the daemon's graph database for their full functionality and return clear error messages when the daemon is unavailable.
4646

4747
### Thread-Safe Index Updates
4848

@@ -74,7 +74,7 @@ The protocol package defines all JSON-RPC 2.0 and MCP message types. Core types
7474

7575
Five tool handlers implement the Handler interface:
7676

77-
**search_files** - Semantic search with query, optional categories filter, and max_results limit. Tries daemon API first, falls back to index search with fuzzy matching.
77+
**search_files** - Semantic search with query, optional categories filter, and max_results limit. Requires daemon for graph-powered search across filenames, tags, topics, and entities.
7878

7979
**get_file_metadata** - Complete metadata for a file by path. Tries daemon API first, falls back to case-insensitive index lookup with substring matching.
8080

@@ -150,7 +150,7 @@ The cmd/mcp/subcommands/start.go command initializes the MCP server. It loads co
150150
A feature supported by the MCP server or client, exchanged during initialization. Server capabilities include resources (with subscribe and listChanged), tools, and prompts.
151151

152152
**Fallback**
153-
The strategy of attempting the daemon API first, then using the in-memory index if unavailable. Enables degraded operation without complete failure.
153+
The strategy of attempting the daemon API first, then using the in-memory index if unavailable. Two tools (get_file_metadata, list_recent_files) support fallback for degraded operation.
154154

155155
**Handler**
156156
An implementation of tool logic that receives dependencies and returns results. Handlers are registered by name and invoked on tools/call requests.

internal/mcp/handlers/search_files.go

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"time"
87

98
"github.com/leefowlercu/agentic-memorizer/internal/mcp/protocol"
10-
"github.com/leefowlercu/agentic-memorizer/internal/search"
119
)
1210

1311
// SearchFilesHandler handles the search_files tool
@@ -47,18 +45,12 @@ func (h *SearchFilesHandler) Execute(ctx context.Context, args json.RawMessage)
4745
params.MaxResults = 10 // default
4846
}
4947

50-
// Try daemon API first if available
51-
if h.deps.HasDaemonAPI() {
52-
result, err := h.executeDaemon(ctx, params)
53-
if err == nil {
54-
return result, nil
55-
}
56-
// Fall through to index-based search on error
57-
h.deps.Logger.Debug("daemon search failed, falling back to index", "error", err)
48+
// Check if daemon API is available
49+
if !h.deps.HasDaemonAPI() {
50+
return nil, fmt.Errorf("daemon API not available; search requires daemon connection")
5851
}
5952

60-
// Fallback to index-based search
61-
return h.executeIndex(params)
53+
return h.executeDaemon(ctx, params)
6254
}
6355

6456
func (h *SearchFilesHandler) executeDaemon(ctx context.Context, params struct {
@@ -119,52 +111,11 @@ func (h *SearchFilesHandler) executeDaemon(ctx context.Context, params struct {
119111
}, nil
120112
}
121113

122-
func (h *SearchFilesHandler) executeIndex(params struct {
123-
Query string `json:"query"`
124-
Categories []string `json:"categories,omitempty"`
125-
MaxResults int `json:"max_results,omitempty"`
126-
}) (any, error) {
127-
index := h.deps.Index.GetIndex()
128-
searcher := search.NewSearcher(index)
129-
results := searcher.Search(search.SearchQuery{
130-
Query: params.Query,
131-
Categories: params.Categories,
132-
MaxResults: params.MaxResults,
133-
})
134-
135-
// Format results
136-
formattedResults := make([]map[string]any, len(results))
137-
for i, result := range results {
138-
formattedResults[i] = map[string]any{
139-
"path": result.File.Path,
140-
"name": result.File.Name,
141-
"category": result.File.Category,
142-
"score": result.Score,
143-
"match_type": result.MatchType,
144-
"size_human": result.File.SizeHuman,
145-
"modified": result.File.Modified.Format(time.RFC3339),
146-
}
147-
148-
// Add semantic fields if available
149-
if result.File.Summary != "" {
150-
formattedResults[i]["summary"] = result.File.Summary
151-
formattedResults[i]["tags"] = result.File.Tags
152-
}
153-
}
154-
155-
return map[string]any{
156-
"query": params.Query,
157-
"result_count": len(results),
158-
"source": "index",
159-
"results": formattedResults,
160-
}, nil
161-
}
162-
163114
// ToolDefinition returns the MCP tool definition
164115
func (h *SearchFilesHandler) ToolDefinition() protocol.Tool {
165116
return protocol.Tool{
166117
Name: "search_files",
167-
Description: "Search for files in the memory index using semantic search. Returns ranked results based on relevance to the query.",
118+
Description: "Search for files in the memory index using semantic search. Returns ranked results based on relevance to the query. Requires FalkorDB to be running.",
168119
InputSchema: protocol.InputSchema{
169120
Schema: "https://json-schema.org/draft/2020-12/schema",
170121
Type: "object",

internal/mcp/integration_test.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"encoding/json"
99
"log/slog"
10+
"strings"
1011
"testing"
1112
"time"
1213

@@ -399,8 +400,8 @@ func TestIntegration_FullToolsFlow(t *testing.T) {
399400
}
400401
})
401402

402-
// Step 4: Call search_files tool
403-
t.Run("tools/call_search_files", func(t *testing.T) {
403+
// Step 4: Call search_files tool (requires daemon, so expect error)
404+
t.Run("tools/call_search_files_requires_daemon", func(t *testing.T) {
404405
callReq := protocol.JSONRPCRequest{
405406
JSONRPC: "2.0",
406407
ID: 3,
@@ -432,22 +433,14 @@ func TestIntegration_FullToolsFlow(t *testing.T) {
432433
t.Fatalf("Failed to unmarshal tools call response: %v", err)
433434
}
434435

435-
if callResp.IsError {
436-
t.Fatalf("Tool returned error: %s", callResp.Content[0].Text)
437-
}
438-
439-
// Parse and verify result
440-
var result map[string]any
441-
if err := json.Unmarshal([]byte(callResp.Content[0].Text), &result); err != nil {
442-
t.Fatalf("Failed to parse result JSON: %v", err)
443-
}
444-
445-
if result["query"] != "terraform" {
446-
t.Errorf("Query = %v, want terraform", result["query"])
436+
// search_files requires daemon, so it should return an error
437+
if !callResp.IsError {
438+
t.Fatal("Expected search_files to return error when daemon is not available")
447439
}
448440

449-
if result["result_count"].(float64) < 1 {
450-
t.Error("Expected at least 1 search result for terraform")
441+
// Verify error message mentions daemon
442+
if !strings.Contains(callResp.Content[0].Text, "daemon") {
443+
t.Errorf("Error message should mention daemon, got: %s", callResp.Content[0].Text)
451444
}
452445
})
453446

internal/mcp/server_test.go

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"log/slog"
8+
"strings"
89
"testing"
910
"time"
1011

@@ -693,7 +694,7 @@ func TestServer_ToolsList(t *testing.T) {
693694
}
694695
}
695696

696-
func TestServer_ToolsCall_SearchFiles(t *testing.T) {
697+
func TestServer_ToolsCall_SearchFiles_RequiresDaemon(t *testing.T) {
697698
index := &types.FileIndex{
698699
Generated: time.Now(),
699700
MemoryRoot: "/test",
@@ -712,7 +713,7 @@ func TestServer_ToolsCall_SearchFiles(t *testing.T) {
712713
}
713714

714715
logger := slog.New(slog.NewTextHandler(bytes.NewBuffer(nil), nil))
715-
server := NewServer(index, logger, "")
716+
server := NewServer(index, logger, "") // No daemon URL
716717
server.initialized = true
717718

718719
writeBuf := bytes.NewBuffer(nil)
@@ -721,7 +722,7 @@ func TestServer_ToolsCall_SearchFiles(t *testing.T) {
721722
writeBuf: writeBuf,
722723
}
723724

724-
// Send tools/call request for search_files
725+
// Send tools/call request for search_files without daemon
725726
request := protocol.JSONRPCRequest{
726727
JSONRPC: "2.0",
727728
ID: 1,
@@ -746,39 +747,31 @@ func TestServer_ToolsCall_SearchFiles(t *testing.T) {
746747
}
747748

748749
if resp.Error != nil {
749-
t.Fatalf("Got error response: %s", resp.Error.Message)
750+
t.Fatalf("Got protocol error response: %s", resp.Error.Message)
750751
}
751752

752-
// Parse tool call response
753+
// Parse tool call response - should indicate error
753754
var callResp protocol.ToolsCallResponse
754755
if err := json.Unmarshal(resp.Result, &callResp); err != nil {
755756
t.Fatalf("Failed to unmarshal tool call response: %v", err)
756757
}
757758

758-
if callResp.IsError {
759-
t.Fatalf("Tool returned error: %s", callResp.Content[0].Text)
759+
// search_files requires daemon, so it should return an error
760+
if !callResp.IsError {
761+
t.Fatal("Expected tool to return error when daemon is not available")
760762
}
761763

762764
if len(callResp.Content) != 1 {
763765
t.Errorf("Content count = %d, want 1", len(callResp.Content))
764766
}
765767

768+
// Verify error message mentions daemon
766769
if callResp.Content[0].Type != "text" {
767770
t.Errorf("Content type = %s, want text", callResp.Content[0].Type)
768771
}
769772

770-
// Verify result contains expected data
771-
var result map[string]any
772-
if err := json.Unmarshal([]byte(callResp.Content[0].Text), &result); err != nil {
773-
t.Fatalf("Failed to unmarshal result JSON: %v", err)
774-
}
775-
776-
if result["query"] != "terraform" {
777-
t.Errorf("Query = %v, want terraform", result["query"])
778-
}
779-
780-
if result["result_count"].(float64) != 1 {
781-
t.Errorf("Result count = %v, want 1", result["result_count"])
773+
if !strings.Contains(callResp.Content[0].Text, "daemon") {
774+
t.Errorf("Error message should mention daemon, got: %s", callResp.Content[0].Text)
782775
}
783776
}
784777

0 commit comments

Comments
 (0)