aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-04-13 12:29:57 +0000
committerMohamed Bassem <me@mbassem.com>2025-04-13 12:31:10 +0000
commite5bacda644ae8b421e9b056bcde4fd57548899c1 (patch)
tree195112503b5b543350b6ee6654b1a0a47d4b9d66
parent1f92ced36ec0af11ec0d10bee6aa2474b46462b6 (diff)
downloadkarakeep-e5bacda644ae8b421e9b056bcde4fd57548899c1.tar.zst
fix(mcp): Reduce token usage of the MCP server
-rw-r--r--apps/mcp/package.json2
-rw-r--r--apps/mcp/src/bookmarks.ts129
-rw-r--r--apps/mcp/src/shared.ts7
-rw-r--r--packages/sdk/README.md4
-rw-r--r--packages/sdk/src/index.ts9
-rw-r--r--pnpm-lock.yaml26
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