diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 3be5b0a..cb06ad7 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -48,6 +48,10 @@ func NewServer(version string, h *Handler) *server.MCPServer { mcp.Description("Max number of source notes to consider (default 5)")), mcp.WithString("category", mcp.Description("Filter to one category: credentials, commands, snippets, decisions, runbooks, learnings, references, contacts, notes")), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + mcp.WithIdempotentHintAnnotation(true), + mcp.WithOpenWorldHintAnnotation(false), ), h.handleSearchNotes, ) @@ -66,6 +70,10 @@ func NewServer(version string, h *Handler) *server.MCPServer { mcp.Description("Filter by a single tag")), mcp.WithNumber("limit", mcp.Description("Max notes to return (default 20)")), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + mcp.WithIdempotentHintAnnotation(true), + mcp.WithOpenWorldHintAnnotation(false), ), h.handleListNotes, ) @@ -80,6 +88,10 @@ func NewServer(version string, h *Handler) *server.MCPServer { ), mcp.WithString("id", mcp.Required(), mcp.Description("Note ID (full UUID or short prefix)")), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + mcp.WithIdempotentHintAnnotation(true), + mcp.WithOpenWorldHintAnnotation(false), ), h.handleGetNote, ) diff --git a/internal/mcp/tools_test.go b/internal/mcp/tools_test.go index 347f212..0dfdfe2 100644 --- a/internal/mcp/tools_test.go +++ b/internal/mcp/tools_test.go @@ -149,6 +149,31 @@ func TestFormatNote_SensitiveMaskedWhenNoReveal(t *testing.T) { } } +func TestNewServer_ToolsAdvertiseReadOnly(t *testing.T) { + // These tools never mutate the KB. MCP clients use the annotation hints to + // decide whether to auto-approve a call, so read-only must be advertised + // honestly — the library defaults (destructive=true, readOnly=false) would + // make clients warn users about safe, read-only lookups. + s := NewServer("test", &Handler{}) + tools := s.ListTools() + for _, name := range []string{"search_notes", "list_notes", "get_note"} { + st, ok := tools[name] + if !ok { + t.Fatalf("tool %q not registered", name) + } + a := st.Tool.Annotations + if a.ReadOnlyHint == nil || !*a.ReadOnlyHint { + t.Errorf("%s: ReadOnlyHint should be true, got %v", name, a.ReadOnlyHint) + } + if a.DestructiveHint == nil || *a.DestructiveHint { + t.Errorf("%s: DestructiveHint should be false, got %v", name, a.DestructiveHint) + } + if a.IdempotentHint == nil || !*a.IdempotentHint { + t.Errorf("%s: IdempotentHint should be true, got %v", name, a.IdempotentHint) + } + } +} + func TestShortID(t *testing.T) { if got := shortID("abcd1234ef56"); got != "abcd1234" { t.Errorf("expected 8-char prefix, got %q", got)