Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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,
)
Expand All @@ -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,
)
Expand Down
25 changes: 25 additions & 0 deletions internal/mcp/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading