import { CallToolResult } from "@modelcontextprotocol/sdk/types"; import { z } from "zod"; import { KarakeepAPISchemas } from "@karakeep/sdk"; import { karakeepClient, mcpServer, toMcpToolError, turndownService, } from "./shared"; interface CompactBookmark { id: string; createdAt: string; title: string; summary: string; note: string; content: | { type: "link"; url: string; description: string; author: string; publisher: string; } | { type: "text"; sourceUrl: string; } | { type: "media"; assetId: string; assetType: string; sourceUrl: string; } | { type: "unknown"; }; tags: string[]; } function compactBookmark( bookmark: KarakeepAPISchemas["Bookmark"], ): CompactBookmark { let content: CompactBookmark["content"]; if (bookmark.content.type === "link") { content = { type: "link", url: bookmark.content.url, description: bookmark.content.description ?? "", author: bookmark.content.author ?? "", publisher: bookmark.content.publisher ?? "", }; } else if (bookmark.content.type === "text") { content = { type: "text", sourceUrl: bookmark.content.sourceUrl ?? "", }; } else if (bookmark.content.type === "asset") { content = { type: "media", assetId: bookmark.content.assetId, assetType: bookmark.content.assetType, sourceUrl: bookmark.content.sourceUrl ?? "", }; } else { content = { type: "unknown", }; } return { id: bookmark.id, createdAt: bookmark.createdAt, title: bookmark.title ? bookmark.title : ((bookmark.content.type === "link" ? bookmark.content.title : undefined) ?? ""), summary: bookmark.summary ?? "", note: bookmark.note ?? "", content, tags: bookmark.tags.map((t) => t.name), }; } // Tools mcpServer.tool( "search-bookmarks", `Search for bookmarks matching a specific a query. `, { query: z.string().describe(` By default, this will do a full-text search, but you can also use qualifiers to filter the results. You can search bookmarks using specific qualifiers. is:fav finds favorited bookmarks, is:archived searches archived bookmarks, is:tagged finds those with tags, is:inlist finds those in lists, and is:link, is:text, and is:media filter by bookmark type. url: searches for URL substrings, # searches for bookmarks with a specific tag, list: searches for bookmarks in a specific list, after: finds bookmarks created on or after a date (YYYY-MM-DD), and before: finds bookmarks created on or before a date (YYYY-MM-DD). If you need to pass names with spaces, you can quote them with double quotes. If you want to negate a qualifier, prefix it with a minus sign. ## Examples: ### Find favorited bookmarks from 2023 that are tagged "important" is:fav after:2023-01-01 before:2023-12-31 #important ### Find archived bookmarks that are either in "reading" list or tagged "work" is:archived and (list:reading or #work) ### Combine text search with qualifiers machine learning is:fav`), }, async ({ query }): Promise => { const res = await karakeepClient.GET("/bookmarks/search", { params: { query: { q: query, limit: 10, }, }, }); if (!res.data) { return toMcpToolError(res.error); } return { content: res.data.bookmarks.map((bookmark) => ({ type: "text", text: JSON.stringify(compactBookmark(bookmark)), })), }; }, ); mcpServer.tool( "get-bookmark", `Get a bookmark by id.`, { bookmarkId: z.string().describe(`The bookmarkId to get.`), }, async ({ bookmarkId }): Promise => { const res = await karakeepClient.GET(`/bookmarks/{bookmarkId}`, { params: { path: { bookmarkId, }, }, }); if (res.error) { return toMcpToolError(res.error); } return { content: [ { type: "text", text: JSON.stringify(compactBookmark(res.data)), }, ], }; }, ); mcpServer.tool( "create-text-bookmark", `Create a text bookmark`, { title: z.string().optional().describe(`The title of the bookmark`), text: z.string().describe(`The text to be bookmarked`), }, async ({ title, text }): Promise => { const res = await karakeepClient.POST(`/bookmarks`, { body: { type: "text", title, text, }, }); if (res.error) { return toMcpToolError(res.error); } return { content: [ { type: "text", text: JSON.stringify(compactBookmark(res.data)), }, ], }; }, ); mcpServer.tool( "create-url-bookmark", `Create a url bookmark`, { title: z.string().optional().describe(`The title of the bookmark`), url: z.string().describe(`The url to be bookmarked`), }, async ({ title, url }): Promise => { const res = await karakeepClient.POST(`/bookmarks`, { body: { type: "link", title, url, }, }); if (res.error) { return toMcpToolError(res.error); } return { content: [ { type: "text", text: JSON.stringify(compactBookmark(res.data)), }, ], }; }, ); mcpServer.tool( "get-bookmark-content", `Get a bookmark content.`, { bookmarkId: z.string().describe(`The bookmarkId to get content for.`), }, async ({ bookmarkId }): Promise => { const res = await karakeepClient.GET(`/bookmarks/{bookmarkId}`, { params: { path: { bookmarkId, }, }, }); if (res.error) { return toMcpToolError(res.error); } let content; if (res.data.content.type === "link") { const htmlContent = res.data.content.htmlContent; content = turndownService.turndown(htmlContent); } else if (res.data.content.type === "text") { content = res.data.content.text; } else if (res.data.content.type === "asset") { content = ""; } return { content: [ { type: "text", text: content ?? "", }, ], }; }, );