|
1 | | -// Package mcp holds the 8 MCP tool handlers + stdio server wiring. |
| 1 | +// Package mcp wires the 8 MCP tool handlers onto the official Go SDK and |
| 2 | +// owns Deps injection + state-aware response shaping. |
| 3 | +// |
2 | 4 | // Spec: |
3 | | -// docs/v04/spec/flows/capture.md (7-phase capture) |
4 | | -// docs/v04/spec/flows/recall.md (7-phase recall) |
5 | | -// docs/v04/spec/flows/lifecycle.md (6 other tools) |
6 | | -// Python: mcp/server/server.py (2002 LoC). |
| 5 | +// docs/v04/spec/components/rune-mcp.md (MCP server 구현) |
| 6 | +// docs/v04/spec/flows/{capture,recall,lifecycle}.md |
| 7 | +// |
| 8 | +// SDK: github.com/modelcontextprotocol/go-sdk v1.5.0+ (D2). Stdio transport. |
| 9 | +// Input schema is auto-inferred from the Go input struct (jsonschema tags |
| 10 | +// optional but recommended; will be tightened in Phase 5). |
7 | 11 | // |
8 | | -// MCP SDK: github.com/modelcontextprotocol/go-sdk v1.5.0+ (D2). |
9 | | -// Stdio transport, tools/call dispatch. Input schema auto-generated from Go |
10 | | -// structs with jsonschema tags. |
| 12 | +// Phase A (current): handshake + tools/list only. Every handler returns a |
| 13 | +// stubResult ("not yet implemented") so Claude Code can discover the catalog |
| 14 | +// without any adapter being wired. Phase 5 replaces each stub with a |
| 15 | +// service-layer call (CheckState → service.X.Handle → response wrap). |
11 | 16 | package mcp |
12 | 17 |
|
13 | 18 | import ( |
14 | 19 | "context" |
15 | 20 |
|
| 21 | + sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" |
| 22 | + |
16 | 23 | "github.com/envector/rune-go/internal/domain" |
| 24 | + "github.com/envector/rune-go/internal/service" |
17 | 25 | ) |
18 | 26 |
|
19 | | -// Deps — injected into all handlers. TODO: fill as adapters stabilize. |
| 27 | +// Deps — injected into all handlers. |
| 28 | +// |
| 29 | +// Phase A: empty struct. Adapter clients · state machine · config will be |
| 30 | +// added as Phase 4 (adapters) and Phase 5 (service orchestration) land. |
| 31 | +// Concrete fields stay commented until each adapter has a real Client type; |
| 32 | +// the handlers below close over deps already so no signature change later. |
20 | 33 | type Deps struct { |
21 | | - // Vault vault.Client |
22 | | - // Envector envector.Client |
23 | | - // Embedder embedder.Client |
| 34 | + // Vault vault.Client |
| 35 | + // Envector envector.Client |
| 36 | + // Embedder embedder.Client |
24 | 37 | // CaptureLog *logio.CaptureLog |
25 | | - // State *lifecycle.Manager |
26 | | - // Cfg *config.Config |
| 38 | + // State *lifecycle.Manager |
| 39 | + // Cfg *config.Config |
27 | 40 | } |
28 | 41 |
|
29 | | -// ───────────────────────────────────────────────────────────────────────────── |
30 | | -// 8 MCP tools — Python bit-identical names/shapes |
31 | | -// ───────────────────────────────────────────────────────────────────────────── |
32 | | - |
33 | | -// ToolCapture — rune_capture. Python: server.py:L698-806 + L1208-1407 _capture_single. |
34 | | -// 7-phase flow (spec/flows/capture.md). |
35 | | -func ToolCapture(ctx context.Context, deps *Deps, req *domain.CaptureRequest) (*domain.CaptureResponse, error) { |
36 | | - // TODO Phase 1: state gate |
37 | | - // TODO Phase 2: validate + tier2 check + text_to_embed pick |
38 | | - // TODO Phase 3: embedder.EmbedSingle(text) |
39 | | - // TODO Phase 4: envector.Score + Vault.DecryptScores → novelty (D11 thresholds) |
40 | | - // TODO Phase 5: record_builder.BuildPhases + embedder.EmbedBatch + AES Seal |
41 | | - // TODO Phase 6: envector.Insert (batch) |
42 | | - // TODO Phase 7: capture_log append + respond |
43 | | - return nil, nil |
44 | | -} |
| 42 | +// emptyArgs — input type for tools that take no arguments. |
| 43 | +type emptyArgs struct{} |
45 | 44 |
|
46 | | -// ToolRecall — rune_recall. Python: server.py:L910-1034. 7-phase. |
47 | | -func ToolRecall(ctx context.Context, deps *Deps, args *domain.RecallArgs) (*domain.RecallResult, error) { |
48 | | - // TODO Phase 1: state gate + topk validation (max 10) |
49 | | - // TODO Phase 2: policy.Parse (English only — D21) |
50 | | - // TODO Phase 3: embedder.EmbedBatch(expansions[:3]) (D22/D23) |
51 | | - // TODO Phase 4: sequential 4-RPC per expansion (D25): Score → DecryptScores |
52 | | - // → GetMetadata → DecryptMetadata (service layer calls Vault directly) |
53 | | - // TODO Phase 5: metadata 3-way classify (AES/plain/legacy base64 per D26) |
54 | | - // TODO Phase 6: phase_chain expansion (D27) + group assemble + filter |
55 | | - // + recency weighting (half-life 90d, status mul) |
56 | | - // TODO Phase 7: build response (synthesized=false per D28) |
57 | | - return nil, nil |
58 | | -} |
| 45 | +// Register binds all 8 MCP tools onto the provided SDK server. |
| 46 | +// |
| 47 | +// Tool naming + ordering are bit-identical to Python `mcp/server/server.py`. |
| 48 | +// Descriptions are intentionally short — Claude reads them in tool selection, |
| 49 | +// not the user, so they should be a single concrete capability sentence. |
| 50 | +func Register(srv *sdkmcp.Server, deps *Deps) { |
| 51 | + // Write tools (state gate applies in Phase 5). |
| 52 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 53 | + Name: "rune_capture", |
| 54 | + Description: "Capture a decision record (agent-delegated extraction required).", |
| 55 | + }, stubHandler[domain.CaptureRequest, domain.CaptureResponse]("rune_capture")) |
59 | 56 |
|
60 | | -// ToolBatchCapture — rune_batch_capture. Python: server.py:L810-896. |
61 | | -// Per-item independent processing (skipped/captured/near_duplicate/error). |
62 | | -func ToolBatchCapture(ctx context.Context, deps *Deps, items []domain.CaptureRequest) (any, error) { |
63 | | - // TODO: per-item _capture_single call + summary (captured/skipped/errors) |
64 | | - return nil, nil |
65 | | -} |
| 57 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 58 | + Name: "rune_batch_capture", |
| 59 | + Description: "Capture a batch of decision records (e.g. session-end sweep).", |
| 60 | + }, stubHandler[service.BatchCaptureArgs, service.BatchCaptureResult]("rune_batch_capture")) |
66 | 61 |
|
67 | | -// ToolCaptureHistory — rune_capture_history. Python: server.py:L1092-1111. |
68 | | -// Read ~/.rune/capture_log.jsonl reverse, filter by domain/since, limit (default 20, max 100). |
69 | | -func ToolCaptureHistory(ctx context.Context, deps *Deps, limit int, domainFilter, since *string) (any, error) { |
70 | | - // TODO: logio.Tail |
71 | | - return nil, nil |
72 | | -} |
| 62 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 63 | + Name: "rune_recall", |
| 64 | + Description: "Query organizational memory by natural-language question.", |
| 65 | + }, stubHandler[domain.RecallArgs, domain.RecallResult]("rune_recall")) |
73 | 66 |
|
74 | | -// ToolDeleteCapture — rune_delete_capture. Python: server.py:L1123-1206. |
75 | | -// Soft-delete: search_by_id → set status=reverted → re-embed → re-insert → log. |
76 | | -func ToolDeleteCapture(ctx context.Context, deps *Deps, recordID string) (any, error) { |
77 | | - // TODO: soft-delete flow |
78 | | - return nil, nil |
79 | | -} |
| 67 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 68 | + Name: "rune_delete_capture", |
| 69 | + Description: "Soft-delete a record by ID (sets status=reverted, re-inserts).", |
| 70 | + }, stubHandler[service.DeleteCaptureArgs, service.DeleteCaptureResult]("rune_delete_capture")) |
80 | 71 |
|
81 | | -// ToolVaultStatus — rune_vault_status. Python: server.py:L496-528. Read-only. |
82 | | -func ToolVaultStatus(ctx context.Context, deps *Deps) (any, error) { |
83 | | - // TODO: vault.HealthCheck + mode/endpoint response |
84 | | - return nil, nil |
85 | | -} |
| 72 | + // Read / diagnostic tools (state gate bypass). |
| 73 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 74 | + Name: "rune_capture_history", |
| 75 | + Description: "List recent captures from local capture_log.jsonl (read-only).", |
| 76 | + }, stubHandler[service.CaptureHistoryArgs, service.CaptureHistoryResult]("rune_capture_history")) |
| 77 | + |
| 78 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 79 | + Name: "rune_vault_status", |
| 80 | + Description: "Probe Vault connectivity and report secure-search mode.", |
| 81 | + }, stubHandler[emptyArgs, service.VaultStatusResult]("rune_vault_status")) |
| 82 | + |
| 83 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 84 | + Name: "rune_diagnostics", |
| 85 | + Description: "Collect a 7-section health snapshot (env / state / vault / keys / pipelines / embedding / envector).", |
| 86 | + }, stubHandler[emptyArgs, service.DiagnosticsResult]("rune_diagnostics")) |
| 87 | + |
| 88 | + sdkmcp.AddTool(srv, &sdkmcp.Tool{ |
| 89 | + Name: "rune_reload_pipelines", |
| 90 | + Description: "Re-initialize Vault + envector pipelines (BOOT replay) with envector warmup.", |
| 91 | + }, stubHandler[emptyArgs, service.ReloadPipelinesResult]("rune_reload_pipelines")) |
86 | 92 |
|
87 | | -// ToolDiagnostics — rune_diagnostics. Python: server.py:L540-684. |
88 | | -// 7 sections: environment / state / vault / keys / pipelines / embedding / envector. |
89 | | -func ToolDiagnostics(ctx context.Context, deps *Deps) (any, error) { |
90 | | - // TODO: collect 7 sections + 5s envector timeout |
91 | | - return nil, nil |
| 93 | + _ = deps // Phase A unused; closures will capture this in Phase 5. |
92 | 94 | } |
93 | 95 |
|
94 | | -// ToolReloadPipelines — rune_reload_pipelines. Python: server.py:L1046-1089. |
95 | | -// Re-init scribe/retriever pipelines + envector GetIndexList warmup (60s timeout). |
96 | | -func ToolReloadPipelines(ctx context.Context, deps *Deps) (any, error) { |
97 | | - // TODO: AwaitInitDone + ReinitPipelines + 60s warmup |
98 | | - return nil, nil |
| 96 | +// stubHandler returns a SDK ToolHandlerFor that always responds with a |
| 97 | +// not-yet-implemented isError result. Output type is preserved so tools/list |
| 98 | +// can still publish the inferred output schema. |
| 99 | +func stubHandler[In, Out any](toolName string) sdkmcp.ToolHandlerFor[In, Out] { |
| 100 | + return func(ctx context.Context, req *sdkmcp.CallToolRequest, in In) (*sdkmcp.CallToolResult, Out, error) { |
| 101 | + _ = ctx |
| 102 | + _ = req |
| 103 | + _ = in |
| 104 | + var zero Out |
| 105 | + return stubResult(toolName), zero, nil |
| 106 | + } |
99 | 107 | } |
100 | 108 |
|
101 | | -// Register — called from main() to bind all 8 tools to the MCP SDK server. |
102 | | -// TODO: uses github.com/modelcontextprotocol/go-sdk/mcp.AddTool for each. |
103 | | -func Register(/* srv *mcp.Server, */ deps *Deps) { |
104 | | - // TODO: |
105 | | - // mcp.AddTool(srv, &mcp.Tool{Name: "rune_capture", ...}, ToolCapture adapter) |
106 | | - // ... 8 tools |
| 109 | +// stubResult composes the Phase-A "not implemented" response. |
| 110 | +func stubResult(toolName string) *sdkmcp.CallToolResult { |
| 111 | + return &sdkmcp.CallToolResult{ |
| 112 | + IsError: true, |
| 113 | + Content: []sdkmcp.Content{ |
| 114 | + &sdkmcp.TextContent{ |
| 115 | + Text: toolName + " is not yet implemented (skeleton phase A — MCP handshake + tools/list only).", |
| 116 | + }, |
| 117 | + }, |
| 118 | + } |
107 | 119 | } |
0 commit comments