From 74b623c63aacaecd3eb06f475fc2722be1783579 Mon Sep 17 00:00:00 2001 From: tianma Date: Wed, 1 Jul 2026 11:53:51 +0800 Subject: [PATCH] Add MCP memo listing tool --- apps/api/src/index.ts | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 36202250..5d6ce4ee 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1693,6 +1693,20 @@ const MCP_TOOLS = [ }, }, }, + { + name: "list_memos", + description: "List active EdgeEver memos with pagination. Use includeContent when full Markdown is needed.", + inputSchema: { + type: "object", + additionalProperties: false, + properties: { + notebookId: { type: "string" }, + limit: { type: "integer", minimum: 1, maximum: 100 }, + offset: { type: "integer", minimum: 0 }, + includeContent: { type: "boolean" }, + }, + }, + }, { name: "get_memo", description: "Read a memo with Markdown content.", @@ -1851,6 +1865,15 @@ const callMcpTool = async ( }), }; } + case "list_memos": { + assertScope(auth, "read:memos"); + return await listMemosForMcp(c.env.DB, { + notebookId: getOptionalString(args.notebookId), + limit: clampNumber(Number(args.limit ?? 50), 1, 100), + offset: clampNumber(Number(args.offset ?? 0), 0, 100_000), + includeContent: args.includeContent === true, + }); + } case "get_memo": { assertScope(auth, "read:memos"); const memoId = getRequiredString(args.memoId, "memoId"); @@ -3084,6 +3107,66 @@ const searchMemoSummaries = async ( return rows.results.map(mapMemoSummary); }; +const listMemosForMcp = async ( + db: D1Database, + options: { notebookId?: string | null; limit: number; offset: number; includeContent: boolean } +) => { + const notebookId = options.notebookId?.trim() || null; + const limit = clampNumber(options.limit, 1, 100); + const offset = clampNumber(options.offset, 0, 100_000); + const pageSize = limit + 1; + + if (options.includeContent) { + const rows = await db + .prepare( + `SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned, + m.is_archived, m.is_deleted, m.created_at, m.updated_at, m.deleted_at, c.revision, + c.content_json, c.content_markdown, c.content_text, c.content_hash, + m.source_memo_ids, m.merge_source_count, m.merged_into_memo_id + FROM memos m + INNER JOIN memo_contents c ON c.memo_id = m.id + WHERE m.is_deleted = 0 + AND (? IS NULL OR m.notebook_id = ?) + ORDER BY m.updated_at DESC, m.id ASC + LIMIT ? OFFSET ?` + ) + .bind(notebookId, notebookId, pageSize, offset) + .all(); + const page = rows.results.slice(0, limit).map(mapMemoDetail); + + return { + memos: page, + limit, + offset, + nextOffset: rows.results.length > limit ? offset + limit : null, + hasMore: rows.results.length > limit, + }; + } + + const rows = await db + .prepare( + `SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned, + m.is_archived, m.is_deleted, m.created_at, m.updated_at, m.deleted_at, c.revision + FROM memos m + INNER JOIN memo_contents c ON c.memo_id = m.id + WHERE m.is_deleted = 0 + AND (? IS NULL OR m.notebook_id = ?) + ORDER BY m.updated_at DESC, m.id ASC + LIMIT ? OFFSET ?` + ) + .bind(notebookId, notebookId, pageSize, offset) + .all(); + const page = rows.results.slice(0, limit).map(mapMemoSummary); + + return { + memos: page, + limit, + offset, + nextOffset: rows.results.length > limit ? offset + limit : null, + hasMore: rows.results.length > limit, + }; +}; + const getNotebook = async (db: D1Database, id: string): Promise => { const row = await db .prepare(