aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-04-13 01:27:45 +0000
committerMohamed Bassem <me@mbassem.com>2025-04-13 01:53:11 +0000
commitcf97bace33fdd14f29ce947d55d17cba8fa85c11 (patch)
tree048a88eeabfcb1b1b32b2cd298c229e6c7082341 /apps
parenta39cd5f3c0a3e287652f945d203facab413b5b65 (diff)
downloadkarakeep-cf97bace33fdd14f29ce947d55d17cba8fa85c11.tar.zst
feat: Add an MCP server for karakeep
Diffstat (limited to 'apps')
-rw-r--r--apps/browser-extension/package.json2
-rw-r--r--apps/mcp/.gitignore1
-rw-r--r--apps/mcp/.npmignore4
-rw-r--r--apps/mcp/README.md34
-rw-r--r--apps/mcp/package.json51
-rw-r--r--apps/mcp/src/bookmarks.ts137
-rw-r--r--apps/mcp/src/index.ts16
-rw-r--r--apps/mcp/src/lists.ts145
-rw-r--r--apps/mcp/src/shared.ts34
-rw-r--r--apps/mcp/src/tags.ts68
-rw-r--r--apps/mcp/tsconfig.json15
-rw-r--r--apps/mcp/vite.config.mts26
-rw-r--r--apps/mobile/package.json2
-rw-r--r--apps/web/package.json2
-rw-r--r--apps/workers/package.json2
15 files changed, 535 insertions, 4 deletions
diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json
index 58da5217..2f4dd7f7 100644
--- a/apps/browser-extension/package.json
+++ b/apps/browser-extension/package.json
@@ -37,7 +37,7 @@
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
- "zod": "^3.22.4"
+ "zod": "^3.24.2"
},
"devDependencies": {
"@crxjs/vite-plugin": "2.0.0-beta.28",
diff --git a/apps/mcp/.gitignore b/apps/mcp/.gitignore
new file mode 100644
index 00000000..1521c8b7
--- /dev/null
+++ b/apps/mcp/.gitignore
@@ -0,0 +1 @@
+dist
diff --git a/apps/mcp/.npmignore b/apps/mcp/.npmignore
new file mode 100644
index 00000000..18a504f5
--- /dev/null
+++ b/apps/mcp/.npmignore
@@ -0,0 +1,4 @@
+.turbo/**
+src/**
+vite.config.mts
+tsconfig.json
diff --git a/apps/mcp/README.md b/apps/mcp/README.md
new file mode 100644
index 00000000..d59bb894
--- /dev/null
+++ b/apps/mcp/README.md
@@ -0,0 +1,34 @@
+# Karakeep MCP Server
+
+This is the Karakeep MCP server, which is a server that can be used to interact with Karakeep from other tools.
+
+## Supported Tools
+
+- Searching bookmarks
+- Adding and removing bookmarks from lists
+- Attaching and detaching tags to bookmarks
+- Creating new lists
+- Creating text and URL bookmarks
+
+Currently, the MCP server only exposes tools (no resources).
+
+## Usage with Claude Desktop
+
+
+```
+{
+ "mcpServers": {
+ "karakeep": {
+ "command": "npx",
+ "args": [
+ "@karakeep/mcp",
+ ],
+ "env": {
+ "KARAKEEP_API_ADDR": "https://<YOUR_SERVER_ADDR>",
+ "KARAKEEP_API_KEY": "<YOUR_TOKEN>"
+ }
+ }
+ }
+}
+```
+
diff --git a/apps/mcp/package.json b/apps/mcp/package.json
new file mode 100644
index 00000000..5e41dd18
--- /dev/null
+++ b/apps/mcp/package.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@karakeep/mcp",
+ "version": "0.23.3",
+ "description": "MCP server for Karakeep",
+ "license": "GNU Affero General Public License version 3",
+ "type": "module",
+ "keywords": [
+ "hoarder",
+ "karakeep",
+ "mcp"
+ ],
+ "bin": {
+ "karakeep-mcp": "dist/index.js"
+ },
+ "devDependencies": {
+ "@karakeep/eslint-config": "workspace:^0.2.0",
+ "@karakeep/prettier-config": "workspace:^0.1.0",
+ "@karakeep/tsconfig": "workspace:^0.1.0",
+ "@tsconfig/node22": "^22.0.0",
+ "shx": "^0.4.0",
+ "tsx": "^4.7.1",
+ "vite": "^5.1.0"
+ },
+ "scripts": {
+ "build": "vite build && shx chmod +x dist/index.js",
+ "run": "tsx src/index.ts",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "format": "prettier . --ignore-path ../../.prettierignore",
+ "format:fix": "prettier . --write --ignore-path ../../.prettierignore",
+ "typecheck": "tsc --noEmit"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/karakeep-app/karakeep.git",
+ "directory": "apps/mcp"
+ },
+ "eslintConfig": {
+ "root": true,
+ "extends": [
+ "@karakeep/eslint-config/base"
+ ]
+ },
+ "prettier": "@karakeep/prettier-config",
+ "dependencies": {
+ "@karakeep/sdk": "workspace:*",
+ "@modelcontextprotocol/sdk": "^1.9.0",
+ "zod": "^3.24.2"
+ }
+}
diff --git a/apps/mcp/src/bookmarks.ts b/apps/mcp/src/bookmarks.ts
new file mode 100644
index 00000000..bcafba91
--- /dev/null
+++ b/apps/mcp/src/bookmarks.ts
@@ -0,0 +1,137 @@
+import { CallToolResult } from "@modelcontextprotocol/sdk/types";
+import { z } from "zod";
+
+import { karakeepClient, mcpServer, toMcpToolError } from "./shared";
+
+// 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:<value> searches for URL substrings, #<tag> searches for bookmarks with a specific tag,
+list:<name> searches for bookmarks in a specific list,
+after:<date> finds bookmarks created on or after a date (YYYY-MM-DD), and before:<date> 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<CallToolResult> => {
+ 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(bookmark),
+ })),
+ };
+ },
+);
+
+mcpServer.tool(
+ "get-bookmark",
+ `Get a bookmark by id.`,
+ {
+ bookmarkId: z.string().describe(`The bookmarkId to get.`),
+ },
+ async ({ bookmarkId }): Promise<CallToolResult> => {
+ const res = await karakeepClient.GET(`/bookmarks/{bookmarkId}`, {
+ params: {
+ path: {
+ bookmarkId,
+ },
+ },
+ });
+ if (res.error) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(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<CallToolResult> => {
+ 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(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<CallToolResult> => {
+ 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(res.data),
+ },
+ ],
+ };
+ },
+);
diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts
new file mode 100644
index 00000000..51437fc6
--- /dev/null
+++ b/apps/mcp/src/index.ts
@@ -0,0 +1,16 @@
+#!/usr/bin/env node
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+
+import { mcpServer } from "./shared";
+
+import "./bookmarks.ts";
+import "./lists.ts";
+import "./tags.ts";
+
+async function run() {
+ // Start receiving messages on stdin and sending messages on stdout
+ const transport = new StdioServerTransport();
+ await mcpServer.connect(transport);
+}
+
+run();
diff --git a/apps/mcp/src/lists.ts b/apps/mcp/src/lists.ts
new file mode 100644
index 00000000..6cfe1d13
--- /dev/null
+++ b/apps/mcp/src/lists.ts
@@ -0,0 +1,145 @@
+import { CallToolResult } from "@modelcontextprotocol/sdk/types";
+import { z } from "zod";
+
+import { karakeepClient, mcpServer, toMcpToolError } from "./shared";
+
+mcpServer.tool(
+ "get-lists",
+ `Search for bookmarks matching a specific a query.`,
+ async (): Promise<CallToolResult> => {
+ const res = await karakeepClient.GET("/lists");
+ if (!res.data) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: res.data.lists.map((list) => ({
+ type: "text",
+ text: JSON.stringify(list),
+ })),
+ };
+ },
+);
+
+mcpServer.tool(
+ "get-bookmarks-in-list",
+ `Search for bookmarks matching a specific a query.`,
+ {
+ listId: z.string().describe(`The listId to search in.`),
+ },
+ async ({ listId }): Promise<CallToolResult> => {
+ const res = await karakeepClient.GET(`/lists/{listId}/bookmarks`, {
+ params: {
+ path: {
+ listId,
+ },
+ },
+ });
+ if (res.error) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: res.data.bookmarks.map((bookmark) => ({
+ type: "text",
+ text: JSON.stringify(bookmark),
+ })),
+ };
+ },
+);
+
+mcpServer.tool(
+ "add-bookmark-to-list",
+ `Add a bookmark to a list.`,
+ {
+ listId: z.string().describe(`The listId to add the bookmark to.`),
+ bookmarkId: z.string().describe(`The bookmarkId to add.`),
+ },
+ async ({ listId, bookmarkId }): Promise<CallToolResult> => {
+ const res = await karakeepClient.PUT(
+ `/lists/{listId}/bookmarks/{bookmarkId}`,
+ {
+ params: {
+ path: {
+ listId,
+ bookmarkId,
+ },
+ },
+ },
+ );
+ if (res.error) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Bookmark ${bookmarkId} added to list ${listId}`,
+ },
+ ],
+ };
+ },
+);
+
+mcpServer.tool(
+ "remove-bookmark-from-list",
+ `Remove a bookmark from a list.`,
+ {
+ listId: z.string().describe(`The listId to remove the bookmark from.`),
+ bookmarkId: z.string().describe(`The bookmarkId to remove.`),
+ },
+ async ({ listId, bookmarkId }): Promise<CallToolResult> => {
+ const res = await karakeepClient.DELETE(
+ `/lists/{listId}/bookmarks/{bookmarkId}`,
+ {
+ params: {
+ path: {
+ listId,
+ bookmarkId,
+ },
+ },
+ },
+ );
+ if (res.error) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Bookmark ${bookmarkId} removed from list ${listId}`,
+ },
+ ],
+ };
+ },
+);
+
+mcpServer.tool(
+ "create-list",
+ `Create a list.`,
+ {
+ name: z.string().describe(`The name of the list.`),
+ icon: z.string().describe(`The emoji icon of the list.`),
+ parentId: z
+ .string()
+ .optional()
+ .describe(`The parent list id of this list.`),
+ },
+ async ({ name, icon }): Promise<CallToolResult> => {
+ const res = await karakeepClient.POST("/lists", {
+ body: {
+ name,
+ icon,
+ },
+ });
+ if (res.error) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: `List ${name} created with id ${res.data.id}`,
+ },
+ ],
+ };
+ },
+);
diff --git a/apps/mcp/src/shared.ts b/apps/mcp/src/shared.ts
new file mode 100644
index 00000000..69672769
--- /dev/null
+++ b/apps/mcp/src/shared.ts
@@ -0,0 +1,34 @@
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { CallToolResult } from "@modelcontextprotocol/sdk/types";
+
+import { createHoarderClient } from "@karakeep/sdk";
+
+const addr = process.env.KARAKEEP_API_ADDR;
+const apiKey = process.env.KARAKEEP_API_KEY;
+
+export const karakeepClient = createHoarderClient({
+ baseUrl: `${addr}/api/v1`,
+ headers: {
+ "Content-Type": "application/json",
+ authorization: `Bearer ${apiKey}`,
+ },
+});
+
+export const mcpServer = new McpServer({
+ name: "Karakeep",
+ version: "0.23.0",
+});
+
+export function toMcpToolError(
+ error: { code: string; message: string } | undefined,
+): CallToolResult {
+ return {
+ isError: true,
+ content: [
+ {
+ type: "text",
+ text: error ? JSON.stringify(error) : `Something went wrong`,
+ },
+ ],
+ };
+}
diff --git a/apps/mcp/src/tags.ts b/apps/mcp/src/tags.ts
new file mode 100644
index 00000000..a23abea0
--- /dev/null
+++ b/apps/mcp/src/tags.ts
@@ -0,0 +1,68 @@
+import { CallToolResult } from "@modelcontextprotocol/sdk/types";
+import { z } from "zod";
+
+import { karakeepClient, mcpServer, toMcpToolError } from "./shared";
+
+mcpServer.tool(
+ "attach-tag-to-bookmark",
+ `Attach a tag to a bookmark.`,
+ {
+ bookmarkId: z.string().describe(`The bookmarkId to attach the tag to.`),
+ tagsToAttach: z.array(z.string()).describe(`The tag names to attach.`),
+ },
+ async ({ bookmarkId, tagsToAttach }): Promise<CallToolResult> => {
+ const res = await karakeepClient.POST(`/bookmarks/{bookmarkId}/tags`, {
+ params: {
+ path: {
+ bookmarkId,
+ },
+ },
+ body: {
+ tags: tagsToAttach.map((tag) => ({ tagName: tag })),
+ },
+ });
+ if (res.error) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Tags ${JSON.stringify(tagsToAttach)} attached to bookmark ${bookmarkId}`,
+ },
+ ],
+ };
+ },
+);
+
+mcpServer.tool(
+ "detach-tag-from-bookmark",
+ `Detach a tag from a bookmark.`,
+ {
+ bookmarkId: z.string().describe(`The bookmarkId to detach the tag from.`),
+ tagsToDetach: z.array(z.string()).describe(`The tag names to detach.`),
+ },
+ async ({ bookmarkId, tagsToDetach }): Promise<CallToolResult> => {
+ const res = await karakeepClient.DELETE(`/bookmarks/{bookmarkId}/tags`, {
+ params: {
+ path: {
+ bookmarkId,
+ },
+ },
+ body: {
+ tags: tagsToDetach.map((tag) => ({ tagName: tag })),
+ },
+ });
+ if (res.error) {
+ return toMcpToolError(res.error);
+ }
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Tags ${JSON.stringify(tagsToDetach)} detached from bookmark ${bookmarkId}`,
+ },
+ ],
+ };
+ },
+);
diff --git a/apps/mcp/tsconfig.json b/apps/mcp/tsconfig.json
new file mode 100644
index 00000000..07b074a3
--- /dev/null
+++ b/apps/mcp/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@karakeep/tsconfig/node.json",
+ "include": ["src", "vite.config.mts"],
+ "exclude": ["node_modules", "dist"],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
+ "strictNullChecks": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "types": ["vite/client"]
+ }
+}
diff --git a/apps/mcp/vite.config.mts b/apps/mcp/vite.config.mts
new file mode 100644
index 00000000..81717bff
--- /dev/null
+++ b/apps/mcp/vite.config.mts
@@ -0,0 +1,26 @@
+// This file is shamelessly copied from immich's CLI vite config
+// https://github.com/immich-app/immich/blob/main/cli/vite.config.ts
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ build: {
+ rollupOptions: {
+ input: "src/index.ts",
+ output: {
+ dir: "dist",
+ },
+ },
+ ssr: true,
+ },
+ ssr: {
+ // bundle everything except for Node built-ins
+ noExternal: /^(?!node:).*$/,
+ },
+ plugins: [tsconfigPaths()],
+ define: {
+ "import.meta.env.CLI_VERSION": JSON.stringify(
+ process.env.npm_package_version,
+ ),
+ },
+});
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index c17d4f26..4ce6a718 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -55,7 +55,7 @@
"react-native-webview": "^13.12.2",
"tailwind-merge": "^2.2.1",
"use-debounce": "^10.0.0",
- "zod": "^3.22.4",
+ "zod": "^3.24.2",
"zustand": "^4.5.1"
},
"devDependencies": {
diff --git a/apps/web/package.json b/apps/web/package.json
index db02bbaa..5279dad8 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -90,7 +90,7 @@
"sharp": "^0.33.3",
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.1",
- "zod": "^3.22.4",
+ "zod": "^3.24.2",
"zustand": "^4.5.1"
},
"devDependencies": {
diff --git a/apps/workers/package.json b/apps/workers/package.json
index 7ce1cbcf..91487973 100644
--- a/apps/workers/package.json
+++ b/apps/workers/package.json
@@ -43,7 +43,7 @@
"tesseract.js": "^5.1.1",
"tsx": "^4.7.1",
"typescript": "^5.7.3",
- "zod": "^3.22.4"
+ "zod": "^3.24.2"
},
"devDependencies": {
"@karakeep/eslint-config": "workspace:^0.2.0",