diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 392efae3e7..85093a475b 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2341,7 +2341,8 @@ "mcp_enabled_description": "Expose a Model Context Protocol (MCP) endpoint so that AI coding assistants (e.g. Claude Code, GitHub Copilot) can read and modify your notes. The endpoint is only accessible from localhost.", "tools": { "search_notes": "Search notes", - "read_note": "Read note", + "get_note": "Get note", + "get_note_content": "Get note content", "update_note_content": "Update note content", "append_to_note": "Append to note", "create_note": "Create note", diff --git a/apps/server/spec/etapi/mcp.spec.ts b/apps/server/spec/etapi/mcp.spec.ts index 2663934b54..d2cb4fee23 100644 --- a/apps/server/spec/etapi/mcp.spec.ts +++ b/apps/server/spec/etapi/mcp.spec.ts @@ -113,7 +113,8 @@ describe("mcp", () => { const body = parseSseResponse(response.text); const toolNames: string[] = body.result.tools.map((t: { name: string }) => t.name); expect(toolNames).toContain("search_notes"); - expect(toolNames).toContain("read_note"); + expect(toolNames).toContain("get_note"); + expect(toolNames).toContain("get_note_content"); expect(toolNames).toContain("create_note"); expect(toolNames).not.toContain("get_current_note"); }); @@ -142,10 +143,26 @@ describe("mcp", () => { expect(content[0].text).toContain(noteId); }); - it("reads a note by ID", async () => { + it("gets note metadata by ID", async () => { const response = await mcpPost(app) .send(jsonRpc("tools/call", { - name: "read_note", + name: "get_note", + arguments: { noteId } + })) + .expect(200); + + const body = parseSseResponse(response.text); + expect(body.result).toBeDefined(); + const parsed = JSON.parse(body.result.content[0].text); + expect(parsed.noteId).toBe(noteId); + expect(parsed.type).toBeDefined(); + expect(parsed.attributes).toBeDefined(); + }); + + it("reads note content by ID", async () => { + const response = await mcpPost(app) + .send(jsonRpc("tools/call", { + name: "get_note_content", arguments: { noteId } })) .expect(200); diff --git a/apps/server/src/services/llm/tools/note_tools.ts b/apps/server/src/services/llm/tools/note_tools.ts index 9fb0fb8a5d..0ee1d0110f 100644 --- a/apps/server/src/services/llm/tools/note_tools.ts +++ b/apps/server/src/services/llm/tools/note_tools.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import becca from "../../../becca/becca.js"; +import mappers from "../../../etapi/mappers.js"; import markdownExport from "../../export/markdown.js"; import markdownImport from "../../import/markdown.js"; import noteService from "../../notes.js"; @@ -84,7 +85,22 @@ export const noteTools = defineTools({ } }, - read_note: { + get_note: { + description: "Get a note's metadata by its ID. Returns title, type, mime, dates, parent/child relationships, and attributes. Does NOT return the note's content — use get_note_content for that.", + inputSchema: z.object({ + noteId: z.string().describe("The ID of the note to retrieve") + }), + execute: async ({ noteId }) => { + const note = becca.getNote(noteId); + if (!note) { + return { error: "Note not found" }; + } + + return mappers.mapNoteToPojo(note); + } + }, + + get_note_content: { description: "Read the full content of a note by its ID. Use search_notes first to find relevant note IDs. Text notes are returned as Markdown.", inputSchema: z.object({ noteId: z.string().describe("The ID of the note to read") @@ -100,8 +116,6 @@ export const noteTools = defineTools({ return { noteId: note.noteId, - title: note.getTitleOrProtected(), - type: note.type, content: getNoteContentForLlm(note) }; }