From 604b608fa88b546c18f0cce57b9083f39c109090 Mon Sep 17 00:00:00 2001 From: dtoro Date: Mon, 13 Jul 2026 10:51:37 +0200 Subject: [PATCH] feat(mcp): expose full knowledge content to the agent, not just snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit search_knowledge and get_entity_knowledge only ever returned a ts_headline snippet/short headline — enough to find a note, not enough to act on it. Add get_knowledge_content(slug), mirroring the web UI's /api/v1/knowledge/content/{id}, so the agent can read a document/ investigation/runbook's full markdown body once it knows which one it needs. upsert_knowledge already covered the write side. Cross-referenced all three tool descriptions so the agent discovers the full-read path. Co-Authored-By: Claude Sonnet 5 --- internal/mcp/server.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 4f50439..2dc51ac 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -149,7 +149,7 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server { ORDER BY ts DESC LIMIT 50`, nStr(args["entity_id"])), nil }) - register(&mcp.Tool{Name: "search_knowledge", Description: "Full-text search across documentation (PostgreSQL FTS with ts_rank ranking)", + register(&mcp.Tool{Name: "search_knowledge", Description: "Full-text search across documentation (PostgreSQL FTS with ts_rank ranking). Returns a short snippet per hit, not the full note — call get_knowledge_content with the returned slug to read the whole thing.", InputSchema: objSchema(prop{"query", "string", "Search terms"}), }, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := argsMap(req) @@ -168,7 +168,7 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server { LIMIT 20`, q), nil }) - register(&mcp.Tool{Name: "get_entity_knowledge", Description: "All documents, investigations, and runbooks linked to an entity", + register(&mcp.Tool{Name: "get_entity_knowledge", Description: "All documents, investigations, and runbooks linked to an entity. Returns a headline per note, not the full text — call get_knowledge_content with the returned slug to read the whole thing.", InputSchema: objSchema(prop{"entity_slug", "string", "Entity slug (e.g. lxc:jellyfin, service:caddy)"}), }, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := argsMap(req) @@ -196,7 +196,19 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server { ORDER BY 1`, slug), nil }) - register(&mcp.Tool{Name: "upsert_knowledge", Description: "Write back what you learned so future sessions (and future you) benefit — this is how the system gets smarter over time. Use it AFTER solving a non-obvious problem, deploying a service, or discovering a gotcha: record the finding, the fix, and any caveats. Re-calling with the same title updates the existing note instead of duplicating. This is the ONLY way to persist knowledge; a chat message alone is forgotten. search_knowledge/get_entity_knowledge read it back.", + register(&mcp.Tool{Name: "get_knowledge_content", Description: "Full markdown body of one document/investigation/runbook, by its own entity slug. search_knowledge and get_entity_knowledge only return short snippets/headlines — once you know which note you need (from either of those, or because you already know its slug), call this to read the whole thing before acting on it.", + InputSchema: objSchema(prop{"slug", "string", "The knowledge entity's own slug (e.g. document:containers/101-jellyfin, runbook:client-enrollment) — not the slug of an entity it's about."}), + }, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := argsMap(req) + slug, _ := args["slug"].(string) + return queryRows(ctx, pool, ` + SELECT ke.title, e.slug, e.type AS kind, ke.content, ke.source, ke.tags, ke.updated_at::text + FROM knowledge_entities ke + JOIN entities e ON e.id = ke.entity_id + WHERE e.slug = $1`, slug), nil + }) + + register(&mcp.Tool{Name: "upsert_knowledge", Description: "Write back what you learned so future sessions (and future you) benefit — this is how the system gets smarter over time. Use it AFTER solving a non-obvious problem, deploying a service, or discovering a gotcha: record the finding, the fix, and any caveats. Re-calling with the same title updates the existing note instead of duplicating. This is the ONLY way to persist knowledge; a chat message alone is forgotten. search_knowledge/get_entity_knowledge find it, get_knowledge_content reads the full body back.", InputSchema: objSchema( prop{"title", "string", "Short, specific, searchable title (e.g. 'Dragonfly memlock rlimit in unprivileged LXCs', not 'notes')."}, prop{"content", "string", "The knowledge itself, in markdown. Be concrete: symptom, root cause, the exact fix/commands, and any caveats. Written for someone hitting this fresh."},