ai-bookmarks-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/index.js +363 -0
  4. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Bookmark Manager Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # ai-bookmarks-mcp
2
+
3
+ Model Context Protocol server that exposes your Chrome bookmarks to AI agents —
4
+ Claude Desktop, [Antigravity](https://antigravity.google/), Cursor, Cline, or
5
+ any other MCP client. Ask your agent to find bookmarks by topic, list your
6
+ folder taxonomy, or look up a specific saved page.
7
+
8
+ Companion to [AI Bookmark Manager](https://github.com/serguei9090/AI-Bookmark-Manager),
9
+ a Chrome extension that tags, summarizes, and organizes bookmarks with AI —
10
+ but this server works standalone: it reads Chrome's native `Bookmarks` file
11
+ directly, no extension required.
12
+
13
+ ## Tools
14
+
15
+ - **search_bookmarks** — keyword search across title/URL/summary/tags. If the
16
+ extension's "Semantic Search" toggle is on and bookmarks have been indexed
17
+ with the Local ONNX embedding provider, results are ranked by vector
18
+ similarity instead, so a query can match by *concept* rather than exact
19
+ words. Semantic scoring runs entirely locally (a small ONNX model,
20
+ downloaded and cached on first use) — no API key, no cloud calls.
21
+ - **list_folders** — the full folder hierarchy and each folder's AI prompt
22
+ context.
23
+ - **get_bookmark** — full details (tags, summary, dates) for one bookmark by
24
+ ID or URL.
25
+
26
+ ## Setup
27
+
28
+ No installation needed — run it with `npx`:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "ai-bookmarks": {
34
+ "command": "npx",
35
+ "args": ["-y", "ai-bookmarks-mcp"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Paste that into:
42
+ - **Claude Desktop**: `%APPDATA%\Claude\claude_desktop_config.json`
43
+ - **Antigravity**: your user or project `mcp_config.json`
44
+ - **Cursor / Cline**: your `mcp.json`
45
+
46
+ By default it auto-detects your Chrome (or Edge/Brave) `Bookmarks` file. To
47
+ point it at a specific file instead, set `BOOKMARKS_DATA_PATH`:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "ai-bookmarks": {
53
+ "command": "npx",
54
+ "args": ["-y", "ai-bookmarks-mcp"],
55
+ "env": { "BOOKMARKS_DATA_PATH": "C:\\path\\to\\Bookmarks" }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## License
62
+
63
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Model Context Protocol (MCP) Server
4
+ *
5
+ * Exposes three primary tools for AI Assistants:
6
+ * 1. search_bookmarks (hybrid keyword and semantic vector search, when the
7
+ * extension's semantic search toggle is on and bookmarks are indexed)
8
+ * 2. list_folders (returns taxonomy and promptContexts)
9
+ * 3. get_bookmark (retrieves tags, summary, and metadata by ID or URL)
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
16
+ const CHUNK_PREFIX = "bm_meta_v1_";
17
+ const METADATA_FOLDER_NAME = ".ai_bookmark_metadata";
18
+ /** Must match LOCAL_MODEL_ID in src/services/embeddingService.ts so query
19
+ * vectors and synced bookmark vectors live in the same embedding space. */
20
+ const LOCAL_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
21
+ function getDefaultChromeBookmarksPath() {
22
+ const home = process.env.HOME || process.env.USERPROFILE || "";
23
+ const localAppData = process.env.LOCALAPPDATA || "";
24
+ const candidates = [
25
+ path.join(localAppData, "Google", "Chrome", "User Data", "Default", "Bookmarks"),
26
+ path.join(home, "Library", "Application Support", "Google", "Chrome", "Default", "Bookmarks"),
27
+ path.join(home, ".config", "google-chrome", "Default", "Bookmarks"),
28
+ path.join(localAppData, "Microsoft", "Edge", "User Data", "Default", "Bookmarks"),
29
+ path.join(localAppData, "BraveSoftware", "Brave-Browser", "User Data", "Default", "Bookmarks"),
30
+ ];
31
+ for (const p of candidates) {
32
+ if (p && fs.existsSync(p))
33
+ return p;
34
+ }
35
+ return null;
36
+ }
37
+ function parseChromeBookmarksFile(filePath) {
38
+ const raw = fs.readFileSync(filePath, "utf-8");
39
+ const data = JSON.parse(raw);
40
+ let metadataFolder = null;
41
+ const findMetaFolder = (node) => {
42
+ if (node.type === "folder" && node.name === METADATA_FOLDER_NAME) {
43
+ metadataFolder = node;
44
+ return;
45
+ }
46
+ if (node.children) {
47
+ for (const child of node.children)
48
+ findMetaFolder(child);
49
+ }
50
+ };
51
+ for (const root of Object.values(data.roots || {})) {
52
+ findMetaFolder(root);
53
+ }
54
+ let syncedMeta = { bookmarks: {}, folders: {} };
55
+ if (metadataFolder && metadataFolder.children) {
56
+ const chunks = [];
57
+ for (const child of metadataFolder.children || []) {
58
+ if (child.name?.startsWith(CHUNK_PREFIX)) {
59
+ const headerEnd = child.name.indexOf(":::");
60
+ if (headerEnd !== -1) {
61
+ const header = child.name.slice(0, headerEnd);
62
+ const content = child.name.slice(headerEnd + 3);
63
+ const parts = header.replace(CHUNK_PREFIX, "").split("_");
64
+ const index = Number.parseInt(parts[0], 10);
65
+ if (!Number.isNaN(index)) {
66
+ chunks.push({ index, data: content });
67
+ }
68
+ }
69
+ }
70
+ }
71
+ chunks.sort((a, b) => a.index - b.index);
72
+ const fullJson = chunks.map((c) => c.data).join("");
73
+ if (fullJson) {
74
+ try {
75
+ syncedMeta = JSON.parse(fullJson);
76
+ }
77
+ catch (e) {
78
+ console.error("[MCP] Error parsing shadow metadata:", e);
79
+ }
80
+ }
81
+ }
82
+ const bookmarksList = [];
83
+ const foldersList = [];
84
+ const traverse = (node, parentId) => {
85
+ if (node.name === METADATA_FOLDER_NAME)
86
+ return;
87
+ if (node.type === "folder") {
88
+ if (node.id !== "0") {
89
+ const meta = syncedMeta.folders[node.id] || {};
90
+ foldersList.push({
91
+ id: node.id,
92
+ parentId,
93
+ name: node.name,
94
+ promptContext: meta.promptContext || "",
95
+ });
96
+ }
97
+ if (node.children) {
98
+ for (const child of node.children)
99
+ traverse(child, node.id);
100
+ }
101
+ }
102
+ else if (node.type === "url") {
103
+ const meta = syncedMeta.bookmarks[node.id] || {};
104
+ bookmarksList.push({
105
+ id: node.id,
106
+ title: node.name,
107
+ url: node.url || "",
108
+ folderId: parentId,
109
+ tags: meta.tags || [],
110
+ summary: meta.summary || "",
111
+ dateAdded: Number(node.date_added) || Date.now(),
112
+ embedding: meta.embedding,
113
+ });
114
+ }
115
+ };
116
+ for (const root of Object.values(data.roots || {})) {
117
+ traverse(root, null);
118
+ }
119
+ return {
120
+ bookmarks: bookmarksList,
121
+ folders: foldersList,
122
+ searchSettings: syncedMeta.searchSettings,
123
+ };
124
+ }
125
+ function loadBookmarkData() {
126
+ // 1. If an explicit file is specified, check if it's Chrome format or JSON export
127
+ const explicitPath = process.env.BOOKMARKS_DATA_PATH;
128
+ if (explicitPath && fs.existsSync(explicitPath)) {
129
+ try {
130
+ const raw = fs.readFileSync(explicitPath, "utf-8");
131
+ const parsed = JSON.parse(raw);
132
+ if (parsed.roots) {
133
+ return parseChromeBookmarksFile(explicitPath);
134
+ }
135
+ if (parsed.bookmarks && parsed.folders) {
136
+ return parsed;
137
+ }
138
+ }
139
+ catch (e) {
140
+ console.error("[MCP] Error reading explicit bookmarks data path:", e);
141
+ }
142
+ }
143
+ // 2. Local exported json file fallback (for development/testing)
144
+ const localExport = path.join(process.cwd(), "bookmarks-data.json");
145
+ if (fs.existsSync(localExport)) {
146
+ try {
147
+ const raw = fs.readFileSync(localExport, "utf-8");
148
+ return JSON.parse(raw);
149
+ }
150
+ catch (e) {
151
+ console.error("[MCP] Error reading local bookmarks-data.json:", e);
152
+ }
153
+ }
154
+ // 3. Auto-detect native Chrome Bookmarks file (Zero configuration!)
155
+ const chromePath = getDefaultChromeBookmarksPath();
156
+ if (chromePath && fs.existsSync(chromePath)) {
157
+ try {
158
+ return parseChromeBookmarksFile(chromePath);
159
+ }
160
+ catch (e) {
161
+ console.error("[MCP] Error auto-parsing Chrome Bookmarks:", e);
162
+ }
163
+ }
164
+ return { bookmarks: [], folders: [] };
165
+ }
166
+ /** Cosine similarity between two equal-length float vectors. */
167
+ function cosineSimilarity(a, b) {
168
+ if (!a || !b || a.length !== b.length || a.length === 0)
169
+ return 0;
170
+ let dot = 0;
171
+ let normA = 0;
172
+ let normB = 0;
173
+ for (let i = 0; i < a.length; i++) {
174
+ dot += a[i] * b[i];
175
+ normA += a[i] * a[i];
176
+ normB += b[i] * b[i];
177
+ }
178
+ if (normA === 0 || normB === 0)
179
+ return 0;
180
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
181
+ }
182
+ let localExtractorPromise = null;
183
+ /**
184
+ * Lazily loads the local MiniLM model. MCP is called by external agents
185
+ * (Claude Desktop, Antigravity, Cursor, ...) that need a self-contained
186
+ * search tool — it deliberately never depends on the bookmark extension's
187
+ * own AI provider/API keys (those are for a separate concern: auto-sort and
188
+ * tagging). The query is always embedded locally, at no cost and no
189
+ * network dependency once this model is cached.
190
+ */
191
+ function getLocalExtractor() {
192
+ if (!localExtractorPromise) {
193
+ localExtractorPromise = import("@huggingface/transformers").then(async ({ pipeline, env }) => {
194
+ env.allowLocalModels = false;
195
+ const extractor = await pipeline("feature-extraction", LOCAL_MODEL_ID);
196
+ return extractor;
197
+ });
198
+ }
199
+ return localExtractorPromise;
200
+ }
201
+ /**
202
+ * Embeds the search query locally so results can be scored against the
203
+ * bookmark vectors synced from the shadow folder. Returns null (never a
204
+ * fabricated vector) when semantic search is off or embedding fails, so
205
+ * callers fall back to plain keyword matching.
206
+ *
207
+ * Note: this only produces matches against bookmarks that were themselves
208
+ * indexed with the Local ONNX provider (384-dim, same model). Bookmarks
209
+ * indexed via the extension's Gemini provider (768-dim) won't line up with
210
+ * this query vector — they're excluded by the dimension check in the caller
211
+ * and fall back to keyword matching for those specific bookmarks.
212
+ */
213
+ async function embedQueryIfEnabled(query, searchSettings) {
214
+ if (!searchSettings?.semanticSearchEnabled)
215
+ return null;
216
+ try {
217
+ const extractor = await getLocalExtractor();
218
+ const output = await extractor(query, { pooling: "mean", normalize: true });
219
+ return Array.from(output.data);
220
+ }
221
+ catch (e) {
222
+ console.error("[MCP] Local query embedding failed:", e);
223
+ return null;
224
+ }
225
+ }
226
+ const server = new Server({
227
+ name: "ai-bookmarks",
228
+ version: "1.0.0",
229
+ }, {
230
+ capabilities: {
231
+ tools: {},
232
+ },
233
+ });
234
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
235
+ return {
236
+ tools: [
237
+ {
238
+ name: "search_bookmarks",
239
+ description: "Search bookmarks by keywords, concepts, tags, or summaries. Uses " +
240
+ "semantic vector search (matching the embedding provider configured " +
241
+ "in the extension) when semantic search is enabled there and " +
242
+ "bookmarks have been indexed; otherwise falls back to keyword matching.",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: {
246
+ query: { type: "string", description: "Search query or topic" },
247
+ limit: {
248
+ type: "number",
249
+ description: "Maximum results (default: 10)",
250
+ },
251
+ },
252
+ required: ["query"],
253
+ },
254
+ },
255
+ {
256
+ name: "list_folders",
257
+ description: "Lists all bookmark folders and their AI-organization prompt contexts.",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {},
261
+ },
262
+ },
263
+ {
264
+ name: "get_bookmark",
265
+ description: "Gets full details of a specific bookmark by ID or URL.",
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: {
269
+ id: { type: "string", description: "Bookmark ID or URL" },
270
+ },
271
+ required: ["id"],
272
+ },
273
+ },
274
+ ],
275
+ };
276
+ });
277
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
278
+ const data = loadBookmarkData();
279
+ if (req.params.name === "list_folders") {
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: JSON.stringify(data.folders, null, 2),
285
+ },
286
+ ],
287
+ };
288
+ }
289
+ if (req.params.name === "get_bookmark") {
290
+ const id = String(req.params.arguments?.id || "");
291
+ const found = data.bookmarks.find((b) => b.id === id || b.url === id);
292
+ if (!found) {
293
+ return {
294
+ content: [
295
+ {
296
+ type: "text",
297
+ text: `No bookmark found matching '${id}'`,
298
+ },
299
+ ],
300
+ };
301
+ }
302
+ return {
303
+ content: [
304
+ {
305
+ type: "text",
306
+ text: JSON.stringify(found, null, 2),
307
+ },
308
+ ],
309
+ };
310
+ }
311
+ if (req.params.name === "search_bookmarks") {
312
+ const rawQuery = String(req.params.arguments?.query || "");
313
+ const query = rawQuery.toLowerCase();
314
+ const limit = Number(req.params.arguments?.limit || 10);
315
+ const keywordMatch = (bm) => bm.title.toLowerCase().includes(query) ||
316
+ bm.url.toLowerCase().includes(query) ||
317
+ bm.summary.toLowerCase().includes(query) ||
318
+ bm.tags.some((t) => t.toLowerCase().includes(query));
319
+ const queryEmbedding = await embedQueryIfEnabled(rawQuery, data.searchSettings);
320
+ let matches;
321
+ if (queryEmbedding && queryEmbedding.length > 0) {
322
+ matches = data.bookmarks
323
+ .map((bm) => {
324
+ const keywordScore = keywordMatch(bm) ? 0.5 : 0;
325
+ const vectorScore = bm.embedding && bm.embedding.length === queryEmbedding.length
326
+ ? Math.max(0, cosineSimilarity(queryEmbedding, bm.embedding))
327
+ : 0;
328
+ return { bm, score: Math.max(keywordScore, vectorScore) };
329
+ })
330
+ .filter((item) => item.score > 0.15)
331
+ .sort((a, b) => b.score - a.score)
332
+ .map((item) => item.bm);
333
+ }
334
+ else {
335
+ matches = data.bookmarks.filter(keywordMatch);
336
+ }
337
+ return {
338
+ content: [
339
+ {
340
+ type: "text",
341
+ text: JSON.stringify(matches.slice(0, limit), null, 2),
342
+ },
343
+ ],
344
+ };
345
+ }
346
+ return {
347
+ content: [
348
+ {
349
+ type: "text",
350
+ text: `Unknown tool: ${req.params.name}`,
351
+ },
352
+ ],
353
+ };
354
+ });
355
+ async function main() {
356
+ const transport = new StdioServerTransport();
357
+ await server.connect(transport);
358
+ console.error("AI Bookmarks MCP server running via stdio");
359
+ }
360
+ main().catch((err) => {
361
+ console.error("Fatal error in MCP server:", err);
362
+ process.exit(1);
363
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "ai-bookmarks-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol server exposing your Chrome bookmarks (keyword + local semantic vector search) to AI agents like Claude Desktop, Antigravity, Cursor, and Cline.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/serguei9090/AI-Bookmark-Manager.git",
10
+ "directory": "mcp-server"
11
+ },
12
+ "homepage": "https://github.com/serguei9090/AI-Bookmark-Manager#readme",
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "bookmarks",
17
+ "chrome-bookmarks",
18
+ "semantic-search",
19
+ "claude"
20
+ ],
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "bin": {
25
+ "ai-bookmarks-mcp": "./dist/index.js"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "start": "node dist/index.js"
30
+ },
31
+ "dependencies": {
32
+ "@huggingface/transformers": "^4.2.0",
33
+ "@modelcontextprotocol/sdk": "^1.5.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.14.0",
37
+ "typescript": "^5.8.2"
38
+ }
39
+ }