diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-04-13 12:29:57 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-04-13 12:31:10 +0000 |
| commit | e5bacda644ae8b421e9b056bcde4fd57548899c1 (patch) | |
| tree | 195112503b5b543350b6ee6654b1a0a47d4b9d66 | |
| parent | 1f92ced36ec0af11ec0d10bee6aa2474b46462b6 (diff) | |
| download | karakeep-e5bacda644ae8b421e9b056bcde4fd57548899c1.tar.zst | |
fix(mcp): Reduce token usage of the MCP server
| -rw-r--r-- | apps/mcp/package.json | 2 | ||||
| -rw-r--r-- | apps/mcp/src/bookmarks.ts | 129 | ||||
| -rw-r--r-- | apps/mcp/src/shared.ts | 7 | ||||
| -rw-r--r-- | packages/sdk/README.md | 4 | ||||
| -rw-r--r-- | packages/sdk/src/index.ts | 9 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 26 |
6 files changed, 167 insertions, 10 deletions
diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 5e41dd18..7fa4e9c4 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -18,6 +18,7 @@ "@karakeep/prettier-config": "workspace:^0.1.0", "@karakeep/tsconfig": "workspace:^0.1.0", "@tsconfig/node22": "^22.0.0", + "@types/turndown": "^5.0.5", "shx": "^0.4.0", "tsx": "^4.7.1", "vite": "^5.1.0" @@ -46,6 +47,7 @@ "dependencies": { "@karakeep/sdk": "workspace:*", "@modelcontextprotocol/sdk": "^1.9.0", + "turndown": "^7.2.0", "zod": "^3.24.2" } } diff --git a/apps/mcp/src/bookmarks.ts b/apps/mcp/src/bookmarks.ts index bcafba91..04e4480d 100644 --- a/apps/mcp/src/bookmarks.ts +++ b/apps/mcp/src/bookmarks.ts @@ -1,7 +1,89 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types"; import { z } from "zod"; -import { karakeepClient, mcpServer, toMcpToolError } from "./shared"; +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( @@ -44,7 +126,7 @@ machine learning is:fav`), return { content: res.data.bookmarks.map((bookmark) => ({ type: "text", - text: JSON.stringify(bookmark), + text: JSON.stringify(compactBookmark(bookmark)), })), }; }, @@ -71,7 +153,7 @@ mcpServer.tool( content: [ { type: "text", - text: JSON.stringify(res.data), + text: JSON.stringify(compactBookmark(res.data)), }, ], }; @@ -100,7 +182,7 @@ mcpServer.tool( content: [ { type: "text", - text: JSON.stringify(res.data), + text: JSON.stringify(compactBookmark(res.data)), }, ], }; @@ -129,7 +211,44 @@ mcpServer.tool( content: [ { type: "text", - text: JSON.stringify(res.data), + 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<CallToolResult> => { + 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 ?? "", }, ], }; diff --git a/apps/mcp/src/shared.ts b/apps/mcp/src/shared.ts index 69672769..2c553d17 100644 --- a/apps/mcp/src/shared.ts +++ b/apps/mcp/src/shared.ts @@ -1,12 +1,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types"; +import TurndownService from "turndown"; -import { createHoarderClient } from "@karakeep/sdk"; +import { createKarakeepClient } from "@karakeep/sdk"; const addr = process.env.KARAKEEP_API_ADDR; const apiKey = process.env.KARAKEEP_API_KEY; -export const karakeepClient = createHoarderClient({ +export const karakeepClient = createKarakeepClient({ baseUrl: `${addr}/api/v1`, headers: { "Content-Type": "application/json", @@ -19,6 +20,8 @@ export const mcpServer = new McpServer({ version: "0.23.0", }); +export const turndownService = new TurndownService(); + export function toMcpToolError( error: { code: string; message: string } | undefined, ): CallToolResult { diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 1bfc10ac..598b56e3 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -11,12 +11,12 @@ npm install @karakeep/sdk ## Usage ```typescript -import { createHoarderClient } from "@karakeep/sdk"; +import { createKarakeepClient } from "@karakeep/sdk"; // Create a client const apiKey = "my-super-secret-key"; const addr = `https://karakeep.mydomain.com`; -const client = createHoarderClient({ +const client = createKarakeepClient({ baseUrl: `${addr}/api/v1/`, headers: { "Content-Type": "application/json", diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 7b148cc6..7ff0c0dc 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,5 +1,12 @@ import createClient from "openapi-fetch"; -import type { paths } from "./hoarder-api.d.ts"; +import type { components, paths } from "./hoarder-api.d.ts"; +/** + * @deprecated Use createKarakeepClient instead. + */ export const createHoarderClient = createClient<paths>; + +export const createKarakeepClient = createClient<paths>; + +export type KarakeepAPISchemas = components["schemas"]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb3b16a7..935ef816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.9.0 version: 1.9.0 + turndown: + specifier: ^7.2.0 + version: 7.2.0 zod: specifier: ^3.24.2 version: 3.24.2 @@ -319,6 +322,9 @@ importers: '@tsconfig/node22': specifier: ^22.0.0 version: 22.0.0 + '@types/turndown': + specifier: ^5.0.5 + version: 5.0.5 shx: specifier: ^0.4.0 version: 0.4.0 @@ -3579,6 +3585,9 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.9.0': resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==} engines: {node: '>=18'} @@ -5338,6 +5347,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/turndown@5.0.5': + resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==} + '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -13498,6 +13510,9 @@ packages: resolution: {integrity: sha512-Jb0rbU4iHEVQ18An/YfakdIv9rKnd3zUfSE117EngrfWXFHo3RndVH96US3GsT8VHpwTncPePDBT2t06PaFLrw==} hasBin: true + turndown@7.2.0: + resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -18087,6 +18102,9 @@ snapshots: '@microsoft/tsdoc@0.15.1': dev: true + '@mixmark-io/domino@2.2.0': + dev: false + '@modelcontextprotocol/sdk@1.9.0': dependencies: content-type: 1.0.5 @@ -20156,6 +20174,9 @@ snapshots: '@types/trusted-types@2.0.7': dev: false + '@types/turndown@5.0.5': + dev: true + '@types/unist@2.0.10': {} '@types/unist@3.0.2': {} @@ -31643,6 +31664,11 @@ snapshots: turbo-windows-arm64: 2.1.2 dev: true + turndown@7.2.0: + dependencies: + '@mixmark-io/domino': 2.2.0 + dev: false + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 |
