@djrcx/raindrop-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.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # Raindrop MCP Server
2
+
3
+ Search and retrieve your curated bookmarks from [Raindrop.io](https://raindrop.io) directly in your AI-powered IDE (Antigravity, Cursor, VS Code, etc.).
4
+
5
+ ## Features
6
+
7
+ - šŸ” Search your bookmarked tools, components, and resources
8
+ - šŸ“‹ List all bookmarks in a collection
9
+ - šŸ“¦ **New:** `search_bookmark_components` - Search UI/component libraries in your bookmarks for specific components and return deep links directly.
10
+ - šŸŽØ **New:** `extract_design_system` - Fetch any URL (or bookmark) and parse CSS variables, custom colors, fonts, and layout metadata to automatically map out its design system.
11
+ - šŸŒ§ļø Auto-updates when you add new bookmarks via browser extension
12
+ - šŸ” Secure credential storage
13
+ - šŸš€ Works cross-platform (Linux, macOS, Windows)
14
+ - ⚔ Automatic IDE configuration
15
+
16
+ ## Tools
17
+
18
+ ### 1. `search_raindrop`
19
+ Search bookmarks in your Raindrop collection.
20
+ * **Arguments:** `query` (string)
21
+
22
+ ### 2. `list_raindrop`
23
+ List all bookmarks in your Raindrop collection.
24
+ * **Arguments:** None
25
+
26
+ ### 3. `search_bookmark_components`
27
+ Deep search matching component keywords (like "navbar", "carousel") inside your bookmarked UI libraries.
28
+ * **Arguments:**
29
+ * `query` (string): The UI component name to find.
30
+ * `collectionId` (string, optional): Raindrop collection ID (defaults to configured collection). Use `"0"` to search all collections.
31
+
32
+ ### 4. `extract_design_system`
33
+ Extract colors, custom variables, fonts, and structural layout indicators from a website URL.
34
+ * **Arguments:**
35
+ * `url` (string): The URL of the website to analyze.
36
+
37
+ ## Quick Start
38
+
39
+ ### 1. Install and Setup (Automatic)
40
+
41
+ ```bash
42
+ npx raindrop-mcp-setup
43
+ ```
44
+
package/dist/config.js ADDED
@@ -0,0 +1,75 @@
1
+ import { homedir } from "os";
2
+ import { join, dirname } from "path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ // Get package name dynamically from package.json
6
+ export let packageName = "raindrop-mcp";
7
+ try {
8
+ const currentDir = dirname(fileURLToPath(import.meta.url));
9
+ const pkgPath = join(currentDir, "..", "package.json");
10
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
11
+ packageName = pkg.name || packageName;
12
+ }
13
+ catch { }
14
+ const CONFIG_DIR = join(homedir(), ".raindrop-mcp");
15
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
16
+ export function ensureConfigDir() {
17
+ if (!existsSync(CONFIG_DIR)) {
18
+ mkdirSync(CONFIG_DIR, { recursive: true });
19
+ }
20
+ }
21
+ export function saveConfig(config) {
22
+ ensureConfigDir();
23
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
24
+ }
25
+ export function loadConfig() {
26
+ if (!existsSync(CONFIG_FILE))
27
+ return null;
28
+ try {
29
+ const data = readFileSync(CONFIG_FILE, "utf-8");
30
+ return JSON.parse(data);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ export function getConfig() {
37
+ // Priority: 1. Env vars, 2. Config file
38
+ const token = process.env.RAINDROP_TOKEN;
39
+ const collectionId = process.env.RAINDROP_COLLECTION_ID;
40
+ if (token) {
41
+ return { token, collectionId: collectionId || "0" };
42
+ }
43
+ const config = loadConfig();
44
+ if (!config) {
45
+ console.error("āŒ No configuration found.");
46
+ console.error("Run: npx raindrop-mcp-setup");
47
+ process.exit(1);
48
+ }
49
+ return config;
50
+ }
51
+ // Generate Antigravity-specific format
52
+ export function generateAntigravityRaindropConfig() {
53
+ const config = getConfig();
54
+ return {
55
+ "$typeName": "exa.cascade_plugins_pb.CascadePluginCommandTemplate",
56
+ "command": "npx",
57
+ "args": ["-y", packageName],
58
+ "env": {
59
+ "RAINDROP_TOKEN": config.token,
60
+ "RAINDROP_COLLECTION_ID": config.collectionId,
61
+ },
62
+ };
63
+ }
64
+ // Generate standard MCP format (for Cursor, VS Code, etc.)
65
+ export function generateStandardRaindropConfig() {
66
+ const config = getConfig();
67
+ return {
68
+ command: "npx",
69
+ args: ["-y", packageName],
70
+ env: {
71
+ RAINDROP_TOKEN: config.token,
72
+ RAINDROP_COLLECTION_ID: config.collectionId,
73
+ },
74
+ };
75
+ }
package/dist/index.js ADDED
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import * as cheerio from "cheerio";
6
+ import { getConfig } from "./config.js";
7
+ const config = getConfig();
8
+ const API_BASE = "https://api.raindrop.io/rest/v1";
9
+ const server = new McpServer({
10
+ name: "Raindrop MCP",
11
+ version: "1.0.0",
12
+ });
13
+ async function fetchRaindrop(endpoint) {
14
+ const res = await fetch(`${API_BASE}${endpoint}`, {
15
+ headers: { Authorization: `Bearer ${config.token}` },
16
+ });
17
+ if (!res.ok) {
18
+ let errMsg = res.statusText;
19
+ try {
20
+ const errJson = await res.json();
21
+ errMsg = errJson.errorMessage || errJson.message || JSON.stringify(errJson);
22
+ }
23
+ catch {
24
+ try {
25
+ const text = await res.text();
26
+ if (text)
27
+ errMsg = text;
28
+ }
29
+ catch { }
30
+ }
31
+ throw new Error(`Raindrop API error (${res.status}): ${errMsg}`);
32
+ }
33
+ return res.json();
34
+ }
35
+ async function postRaindrop(endpoint, body) {
36
+ const res = await fetch(`${API_BASE}${endpoint}`, {
37
+ method: "POST",
38
+ headers: {
39
+ "Authorization": `Bearer ${config.token}`,
40
+ "Content-Type": "application/json",
41
+ },
42
+ body: JSON.stringify(body),
43
+ });
44
+ if (!res.ok) {
45
+ let errMsg = res.statusText;
46
+ try {
47
+ const errJson = await res.json();
48
+ errMsg = errJson.errorMessage || errJson.message || JSON.stringify(errJson);
49
+ }
50
+ catch {
51
+ try {
52
+ const text = await res.text();
53
+ if (text)
54
+ errMsg = text;
55
+ }
56
+ catch { }
57
+ }
58
+ throw new Error(`Raindrop API error (${res.status}): ${errMsg}`);
59
+ }
60
+ return res.json();
61
+ }
62
+ async function fetchWithTimeout(url, timeoutMs = 5000) {
63
+ const controller = new AbortController();
64
+ const id = setTimeout(() => controller.abort(), timeoutMs);
65
+ try {
66
+ const res = await fetch(url, { signal: controller.signal });
67
+ clearTimeout(id);
68
+ if (!res.ok)
69
+ throw new Error(`HTTP error: ${res.status} ${res.statusText}`);
70
+ return await res.text();
71
+ }
72
+ catch (err) {
73
+ clearTimeout(id);
74
+ throw err;
75
+ }
76
+ }
77
+ // 1. search_bookmarks (Full original schema implementation)
78
+ server.tool("search_bookmarks", "Search bookmarks from Raindrop.io", {
79
+ search: z.string().describe("The search query"),
80
+ collection_id: z.number().optional().default(0).describe("The collection ID to filter bookmarks, 0 for all, -1 for unsorted, -99 for trash, others for specific collection"),
81
+ page: z.number().optional().default(0).describe("The page number"),
82
+ perpage: z.number().optional().default(50).describe("The number of bookmarks per page"),
83
+ sort: z.enum(["-created", "created"]).optional().describe("Sort bookmarks"),
84
+ }, async ({ search, collection_id, page, perpage, sort }) => {
85
+ try {
86
+ const activeCollection = collection_id !== undefined ? collection_id : parseInt(config.collectionId, 10);
87
+ let endpoint = `/raindrops/${activeCollection}?search=${encodeURIComponent(search)}&perpage=${perpage}&page=${page}`;
88
+ if (sort) {
89
+ endpoint += `&sort=${sort}`;
90
+ }
91
+ const data = await fetchRaindrop(endpoint);
92
+ if (!data.items || data.items.length === 0) {
93
+ return { content: [{ type: "text", text: "No bookmarks found for that query." }] };
94
+ }
95
+ const formatted = data.items
96
+ .map((item) => `### ${item.title}\n` +
97
+ `- **URL:** ${item.link}\n` +
98
+ `- **Tags:** ${item.tags.join(", ") || "None"}\n` +
99
+ `- **Excerpt:** ${item.excerpt || "No description."}\n`)
100
+ .join("\n---\n");
101
+ return {
102
+ content: [{ type: "text", text: `Found ${data.items.length} bookmarks:\n\n${formatted}` }],
103
+ };
104
+ }
105
+ catch (error) {
106
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
107
+ }
108
+ });
109
+ // 2. create_bookmarks (Full original schema implementation)
110
+ server.tool("create_bookmarks", "Create bookmarks on Raindrop.io", {
111
+ items: z.array(z.object({
112
+ link: z.string().url().describe("The URL of this bookmark"),
113
+ title: z.string().optional().describe("The bookmark title"),
114
+ excerpt: z.string().optional().describe("The excerpt"),
115
+ })).describe("The bookmarks to create"),
116
+ }, async ({ items }) => {
117
+ try {
118
+ const collectionIdNum = parseInt(config.collectionId, 10) || 0;
119
+ const body = {
120
+ items: items.map((item) => {
121
+ const itemBody = {
122
+ link: item.link,
123
+ collection: {
124
+ $id: collectionIdNum,
125
+ },
126
+ pleaseParse: {}, // Tell Raindrop to crawl title/excerpt/cover if empty
127
+ };
128
+ if (item.title)
129
+ itemBody.title = item.title;
130
+ if (item.excerpt)
131
+ itemBody.excerpt = item.excerpt;
132
+ return itemBody;
133
+ }),
134
+ };
135
+ const res = await postRaindrop("/raindrops", body);
136
+ if (!res.items || res.items.length === 0) {
137
+ return { content: [{ type: "text", text: "Failed to create bookmarks." }] };
138
+ }
139
+ const formatted = res.items
140
+ .map((item) => `- **${item.title || item.link}** (${item.link})`)
141
+ .join("\n");
142
+ return {
143
+ content: [{ type: "text", text: `āœ… Created ${res.items.length} bookmark(s) successfully:\n\n${formatted}` }],
144
+ };
145
+ }
146
+ catch (error) {
147
+ return { content: [{ type: "text", text: `Error creating bookmarks: ${error.message}` }] };
148
+ }
149
+ });
150
+ // 3. get_collections (Full original schema implementation)
151
+ server.tool("get_collections", "Get collections from Raindrop.io", {}, async () => {
152
+ try {
153
+ const data = await fetchRaindrop("/collections");
154
+ if (!data.items || data.items.length === 0) {
155
+ return { content: [{ type: "text", text: "No collections found." }] };
156
+ }
157
+ const formatted = data.items
158
+ .map((item) => `- **${item.title}** (ID: \`${item._id}\`, Count: ${item.count})`)
159
+ .join("\n");
160
+ return {
161
+ content: [{ type: "text", text: `Found ${data.items.length} collections:\n\n${formatted}` }],
162
+ };
163
+ }
164
+ catch (error) {
165
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
166
+ }
167
+ });
168
+ // 4. search_raindrop (Legacy alias support)
169
+ server.tool("search_raindrop", "Search bookmarks in your Raindrop collection. Use this to find curated tools, resources, and references.", { query: z.string().describe("Search term (searches title, description, tags, URL)") }, async ({ query }) => {
170
+ try {
171
+ const data = await fetchRaindrop(`/raindrops/${config.collectionId}?search=${encodeURIComponent(query)}&perpage=10`);
172
+ if (!data.items || data.items.length === 0) {
173
+ return { content: [{ type: "text", text: "No bookmarks found for that query." }] };
174
+ }
175
+ const formatted = data.items
176
+ .map((item) => `### ${item.title}\n` +
177
+ `- **URL:** ${item.link}\n` +
178
+ `- **Tags:** ${item.tags.join(", ") || "None"}\n` +
179
+ `- **Excerpt:** ${item.excerpt || "No description."}\n`)
180
+ .join("\n---\n");
181
+ return {
182
+ content: [{ type: "text", text: `Found ${data.items.length} bookmarks:\n\n${formatted}` }],
183
+ };
184
+ }
185
+ catch (error) {
186
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
187
+ }
188
+ });
189
+ // 5. list_raindrop (Legacy support)
190
+ server.tool("list_raindrop", "List all bookmarks in your Raindrop collection.", {}, async () => {
191
+ try {
192
+ const data = await fetchRaindrop(`/raindrops/${config.collectionId}?perpage=50`);
193
+ const formatted = data.items
194
+ .map((item) => `- **${item.title}** (${item.tags.join(", ") || "untagged"}): ${item.link}`)
195
+ .join("\n");
196
+ return { content: [{ type: "text", text: `Bookmarks:\n${formatted}` }] };
197
+ }
198
+ catch (error) {
199
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
200
+ }
201
+ });
202
+ // 6. extract_design_system
203
+ server.tool("extract_design_system", "Extract the design system (colors, typography, CSS variables, and layout patterns) from a bookmarked URL or any website.", { url: z.string().url().describe("The URL of the website to analyze") }, async ({ url }) => {
204
+ try {
205
+ const html = await fetchWithTimeout(url, 6000);
206
+ const $ = cheerio.load(html);
207
+ const designSystem = {
208
+ title: $("title").text().trim() || url,
209
+ fonts: [],
210
+ cssVariables: {},
211
+ colors: [],
212
+ tailwindDetected: false,
213
+ layoutElements: [],
214
+ };
215
+ // Detect Google Fonts or other font imports
216
+ $('link[href*="fonts.googleapis.com"], link[href*="fonts.gstatic.com"]').each((_, el) => {
217
+ const href = $(el).attr("href");
218
+ if (href)
219
+ designSystem.fonts.push(href);
220
+ });
221
+ // Detect Tailwind CSS
222
+ $('link[href*="tailwind"], script[src*="tailwind"]').each((_, el) => {
223
+ designSystem.tailwindDetected = true;
224
+ });
225
+ if (html.includes("tailwindcss") || html.includes("tailwind.config")) {
226
+ designSystem.tailwindDetected = true;
227
+ }
228
+ // Check common layout structures
229
+ const layoutTags = ["nav", "header", "footer", "main", "aside", "section"];
230
+ layoutTags.forEach(tag => {
231
+ if ($(tag).length > 0) {
232
+ designSystem.layoutElements.push(`${tag} (${$(tag).length} instances)`);
233
+ }
234
+ });
235
+ // Extract inline style variables and style tag content
236
+ let cssText = "";
237
+ $("style").each((_, el) => {
238
+ cssText += $(el).text() + "\n";
239
+ });
240
+ $("[style]").each((_, el) => {
241
+ cssText += $(el).attr("style") + "\n";
242
+ });
243
+ // Fetch external stylesheets (max 2 to avoid blocking/timeouts)
244
+ const stylesheetUrls = [];
245
+ $('link[rel="stylesheet"]').each((_, el) => {
246
+ const href = $(el).attr("href");
247
+ if (href && stylesheetUrls.length < 2) {
248
+ try {
249
+ stylesheetUrls.push(new URL(href, url).toString());
250
+ }
251
+ catch { }
252
+ }
253
+ });
254
+ for (const sheetUrl of stylesheetUrls) {
255
+ try {
256
+ if (!sheetUrl.includes("font-awesome") && !sheetUrl.includes("bootstrap")) {
257
+ const sheetText = await fetchWithTimeout(sheetUrl, 3000);
258
+ cssText += sheetText + "\n";
259
+ }
260
+ }
261
+ catch (e) {
262
+ // Continue silently
263
+ }
264
+ }
265
+ // Parse CSS Variables
266
+ const varRegex = /(--[\w-]+)\s*:\s*([^;}]+)/g;
267
+ let match;
268
+ while ((match = varRegex.exec(cssText)) !== null) {
269
+ const key = match[1].trim();
270
+ const value = match[2].trim();
271
+ if (Object.keys(designSystem.cssVariables).length < 60) {
272
+ designSystem.cssVariables[key] = value;
273
+ }
274
+ }
275
+ // Parse colors (hex, rgb, hsl)
276
+ const hexRegex = /#([0-9a-fA-F]{3,8})\b/g;
277
+ const rgbRegex = /rgb\([^)]+\)/g;
278
+ const hslRegex = /hsl\([^)]+\)/g;
279
+ const foundColors = new Set();
280
+ let colMatch;
281
+ while ((colMatch = hexRegex.exec(cssText)) !== null) {
282
+ foundColors.add(colMatch[0]);
283
+ if (foundColors.size >= 25)
284
+ break;
285
+ }
286
+ if (foundColors.size < 25) {
287
+ while ((colMatch = rgbRegex.exec(cssText)) !== null) {
288
+ foundColors.add(colMatch[0]);
289
+ if (foundColors.size >= 25)
290
+ break;
291
+ }
292
+ }
293
+ if (foundColors.size < 25) {
294
+ while ((colMatch = hslRegex.exec(cssText)) !== null) {
295
+ foundColors.add(colMatch[0]);
296
+ if (foundColors.size >= 25)
297
+ break;
298
+ }
299
+ }
300
+ designSystem.colors = Array.from(foundColors);
301
+ let md = `## Design System for: [${designSystem.title}](${url})\n\n`;
302
+ md += `### šŸŽØ Color Palette (Extracted Colors)\n`;
303
+ if (designSystem.colors.length > 0) {
304
+ md += designSystem.colors.map((c) => `- \`${c}\``).join("\n") + "\n\n";
305
+ }
306
+ else {
307
+ md += `- None directly identified.\n\n`;
308
+ }
309
+ md += `### āš™ļø CSS Custom Variables\n`;
310
+ const variableEntries = Object.entries(designSystem.cssVariables);
311
+ if (variableEntries.length > 0) {
312
+ md += variableEntries.map(([k, v]) => `- \`${k}\`: \`${v}\``).join("\n") + "\n\n";
313
+ }
314
+ else {
315
+ md += `- No CSS variables found.\n\n`;
316
+ }
317
+ md += `### šŸ…°ļø Typography & Fonts\n`;
318
+ if (designSystem.fonts.length > 0) {
319
+ md += designSystem.fonts.map((f) => `- Google Fonts/external font import: [Font Link](${f})`).join("\n") + "\n\n";
320
+ }
321
+ else {
322
+ md += `- No external font libraries detected. Standard system fonts or custom @font-face are likely used.\n\n`;
323
+ }
324
+ md += `### šŸ“ Layout & Frameworks\n`;
325
+ md += `- **Tailwind CSS Detected:** ${designSystem.tailwindDetected ? "Yes" : "No"}\n`;
326
+ if (designSystem.layoutElements.length > 0) {
327
+ md += `- **Semantic Elements:**\n` + designSystem.layoutElements.map((el) => ` - ${el}`).join("\n") + "\n";
328
+ }
329
+ return {
330
+ content: [{ type: "text", text: md }],
331
+ };
332
+ }
333
+ catch (error) {
334
+ return { content: [{ type: "text", text: `Error extracting design system from ${url}: ${error.message}` }] };
335
+ }
336
+ });
337
+ // 7. search_bookmark_components
338
+ server.tool("search_bookmark_components", "Search UI/component libraries in your bookmarks for specific components and return direct links.", {
339
+ query: z.string().describe("The component to search for (e.g. 'navbar', 'hero', 'carousel')"),
340
+ collectionId: z.string().optional().describe("Raindrop collection ID (defaults to configured collection)"),
341
+ }, async ({ query, collectionId }) => {
342
+ try {
343
+ const activeCollection = collectionId || config.collectionId;
344
+ const data = await fetchRaindrop(`/raindrops/${activeCollection}?perpage=50`);
345
+ if (data.items.length === 0) {
346
+ return { content: [{ type: "text", text: "No bookmarks found to search components in." }] };
347
+ }
348
+ const results = [];
349
+ // Filter bookmarks that look like UI/component libraries
350
+ const targets = data.items.filter((item) => {
351
+ const title = item.title.toLowerCase();
352
+ const excerpt = (item.excerpt || "").toLowerCase();
353
+ const tags = item.tags.map((t) => t.toLowerCase());
354
+ const link = item.link.toLowerCase();
355
+ return (title.includes("ui") ||
356
+ title.includes("component") ||
357
+ title.includes("library") ||
358
+ title.includes("kit") ||
359
+ title.includes("template") ||
360
+ excerpt.includes("ui") ||
361
+ excerpt.includes("component") ||
362
+ tags.includes("ui") ||
363
+ tags.includes("components") ||
364
+ tags.includes("ui-library") ||
365
+ link.includes("ui") ||
366
+ link.includes("component"));
367
+ });
368
+ const itemsToSearch = targets.length > 0 ? targets : data.items.slice(0, 5);
369
+ const fetchPromises = itemsToSearch.slice(0, 8).map(async (item) => {
370
+ try {
371
+ const html = await fetchWithTimeout(item.link, 4500);
372
+ const $ = cheerio.load(html);
373
+ const matches = [];
374
+ $("a").each((_, el) => {
375
+ const href = $(el).attr("href");
376
+ const text = $(el).text().trim().toLowerCase();
377
+ if (href && (text.includes(query.toLowerCase()) || href.toLowerCase().includes(query.toLowerCase()))) {
378
+ try {
379
+ const absoluteUrl = new URL(href, item.link).toString();
380
+ if (!matches.some(m => m.href === absoluteUrl)) {
381
+ matches.push({ text: $(el).text().trim() || href, href: absoluteUrl });
382
+ }
383
+ }
384
+ catch { }
385
+ }
386
+ });
387
+ return {
388
+ title: item.title,
389
+ baseUrl: item.link,
390
+ matches: matches.slice(0, 8),
391
+ };
392
+ }
393
+ catch (e) {
394
+ return {
395
+ title: item.title,
396
+ baseUrl: item.link,
397
+ matches: [],
398
+ error: e.message,
399
+ };
400
+ }
401
+ });
402
+ const fetchResults = await Promise.all(fetchPromises);
403
+ let output = `## Component Matches for "${query}" in Bookmarked Libraries:\n\n`;
404
+ let foundAny = false;
405
+ for (const res of fetchResults) {
406
+ if (res.matches.length > 0) {
407
+ foundAny = true;
408
+ output += `### šŸ“ [${res.title}](${res.baseUrl})\n`;
409
+ res.matches.forEach((m) => {
410
+ output += `- [${m.text}](${m.href})\n`;
411
+ });
412
+ output += "\n";
413
+ }
414
+ }
415
+ if (!foundAny) {
416
+ output += `No specific component links matching "${query}" were found on the homepages of the following searched libraries:\n`;
417
+ itemsToSearch.forEach((item) => {
418
+ output += `- [${item.title}](${item.link})\n`;
419
+ });
420
+ }
421
+ return {
422
+ content: [{ type: "text", text: output }],
423
+ };
424
+ }
425
+ catch (error) {
426
+ return { content: [{ type: "text", text: `Error searching component bookmarks: ${error.message}` }] };
427
+ }
428
+ });
429
+ async function main() {
430
+ const transport = new StdioServerTransport();
431
+ await server.connect(transport);
432
+ console.error(`āœ… Raindrop MCP running (collection: ${config.collectionId})`);
433
+ }
434
+ main().catch((error) => {
435
+ console.error("Fatal:", error);
436
+ process.exit(1);
437
+ });
package/dist/setup.js ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ import inquirer from "inquirer";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+ import { existsSync, readFileSync, writeFileSync } from "fs";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ import { saveConfig, generateAntigravityRaindropConfig, generateStandardRaindropConfig, packageName } from "./config.js";
9
+ async function main() {
10
+ if (!process.stdin.isTTY) {
11
+ console.log("āš ļø Non-interactive terminal detected. Skipping automatic Raindrop MCP configuration.");
12
+ console.log(`You can configure it manually later by running: npx ${packageName}-setup`);
13
+ process.exit(0);
14
+ }
15
+ console.log(chalk.bold.cyan("\nšŸŒ§ļø Raindrop MCP Setup\n"));
16
+ // Step 1: Get credentials
17
+ const answers = await inquirer.prompt([
18
+ {
19
+ type: "password",
20
+ name: "token",
21
+ message: "Enter your Raindrop API token:",
22
+ validate: (input) => (input.length > 0 ? true : "Token is required"),
23
+ },
24
+ {
25
+ type: "input",
26
+ name: "collectionId",
27
+ message: "Enter your Raindrop Collection ID (or '0' for all bookmarks):",
28
+ default: "0",
29
+ validate: (input) => (/^\d+$/.test(input) ? true : "Must be a number"),
30
+ },
31
+ ]);
32
+ const spinner = ora("Saving configuration...").start();
33
+ try {
34
+ // Save credentials
35
+ saveConfig({
36
+ token: answers.token,
37
+ collectionId: answers.collectionId,
38
+ });
39
+ spinner.succeed(chalk.green("Configuration saved to ~/.raindrop-mcp/config.json"));
40
+ // Step 2: Ask about auto-configuring IDE
41
+ const ideChoice = await inquirer.prompt([
42
+ {
43
+ type: "confirm",
44
+ name: "autoConfig",
45
+ message: "Would you like to automatically add this to your IDE's MCP config?",
46
+ default: true,
47
+ },
48
+ {
49
+ type: "list",
50
+ name: "ide",
51
+ message: "Which IDE are you using?",
52
+ choices: ["Antigravity", "Cursor", "VS Code", "Manual (show me the config)"],
53
+ when: (ans) => ans.autoConfig,
54
+ },
55
+ ]);
56
+ if (ideChoice.autoConfig && ideChoice.ide !== "Manual (show me the config)") {
57
+ const configPath = await getConfigPath(ideChoice.ide);
58
+ if (configPath) {
59
+ const updateSpinner = ora(`Updating ${configPath}...`).start();
60
+ try {
61
+ await updateMCPConfig(configPath, ideChoice.ide);
62
+ updateSpinner.succeed(chalk.green(`āœ… Updated ${configPath}`));
63
+ console.log(chalk.bold.green("\nšŸŽ‰ Setup complete! Restart your IDE to use Raindrop MCP.\n"));
64
+ }
65
+ catch (error) {
66
+ updateSpinner.fail(chalk.red(`Failed to update config: ${error.message}`));
67
+ console.log(chalk.yellow("\nYou can manually add this to your config:\n"));
68
+ console.log(JSON.stringify({ raindrop: generateAntigravityRaindropConfig() }, null, 1));
69
+ }
70
+ }
71
+ }
72
+ else {
73
+ // Manual mode - just show the config
74
+ console.log(chalk.bold("\nšŸ“‹ Add this to your IDE's MCP config:\n"));
75
+ console.log(chalk.gray("─".repeat(60)));
76
+ console.log(JSON.stringify({ raindrop: generateStandardRaindropConfig() }, null, 2));
77
+ console.log(chalk.gray("─".repeat(60)));
78
+ console.log(chalk.bold.green("\nāœ… Setup complete!\n"));
79
+ }
80
+ }
81
+ catch (error) {
82
+ spinner.fail(chalk.red("Setup failed"));
83
+ console.error(error);
84
+ process.exit(1);
85
+ }
86
+ }
87
+ async function getConfigPath(ide) {
88
+ const commonPaths = {
89
+ Antigravity: [
90
+ join(homedir(), ".gemini", "config", "mcp_config.json"),
91
+ join(homedir(), ".config", "antigravity", "mcp_config.json"),
92
+ join(homedir(), ".antigravity", "mcp_config.json"),
93
+ join(homedir(), "Library", "Application Support", "Antigravity", "mcp_config.json"),
94
+ ],
95
+ Cursor: [
96
+ join(homedir(), ".config", "Cursor", "User", "globalStorage", "mcp.json"),
97
+ join(homedir(), ".cursor", "mcp.json"),
98
+ ],
99
+ "VS Code": [
100
+ join(homedir(), ".config", "Code", "User", "globalStorage", "mcp.json"),
101
+ ],
102
+ };
103
+ const paths = commonPaths[ide] || [];
104
+ for (const path of paths) {
105
+ if (existsSync(path)) {
106
+ const { usePath } = await inquirer.prompt([
107
+ {
108
+ type: "confirm",
109
+ name: "usePath",
110
+ message: `Found config at ${path}. Use this?`,
111
+ default: true,
112
+ },
113
+ ]);
114
+ if (usePath)
115
+ return path;
116
+ }
117
+ }
118
+ const { customPath } = await inquirer.prompt([
119
+ {
120
+ type: "input",
121
+ name: "customPath",
122
+ message: `Enter the path to your ${ide} MCP config file:`,
123
+ validate: (input) => (input ? true : "Path is required"),
124
+ },
125
+ ]);
126
+ return customPath;
127
+ }
128
+ async function updateMCPConfig(configPath, ide) {
129
+ let config = {};
130
+ // Read existing config if it exists
131
+ if (existsSync(configPath)) {
132
+ try {
133
+ const content = readFileSync(configPath, "utf-8");
134
+ config = JSON.parse(content);
135
+ }
136
+ catch (error) {
137
+ throw new Error(`Failed to parse existing config: ${error}`);
138
+ }
139
+ }
140
+ // Find the mcpServers key (with or without trailing space)
141
+ const mcpServersKey = Object.keys(config).find((key) => key.trim() === "mcpServers") || "mcpServers ";
142
+ if (!config[mcpServersKey]) {
143
+ config[mcpServersKey] = {};
144
+ }
145
+ // Add raindrop config in the appropriate format
146
+ if (ide === "Antigravity") {
147
+ config[mcpServersKey]["raindrop "] = generateAntigravityRaindropConfig();
148
+ }
149
+ else {
150
+ config[mcpServersKey]["raindrop"] = generateStandardRaindropConfig();
151
+ }
152
+ // Write back with proper formatting
153
+ writeFileSync(configPath, JSON.stringify(config, null, 1));
154
+ }
155
+ main().catch((error) => {
156
+ console.error(chalk.red("Fatal error:"), error);
157
+ process.exit(1);
158
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@djrcx/raindrop-mcp",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.0",
7
+ "description": "MCP server to search Raindrop.io bookmarks from AI IDEs",
8
+ "type": "module",
9
+ "bin": {
10
+ "raindrop-mcp": "dist/index.js",
11
+ "raindrop-mcp-setup": "dist/setup.js"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc && chmod +x dist/index.js && chmod +x dist/setup.js",
19
+ "start": "node dist/index.js",
20
+ "setup": "node dist/setup.js",
21
+ "postinstall": "node dist/setup.js",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "raindrop",
27
+ "bookmarks",
28
+ "ai",
29
+ "cursor",
30
+ "vscode",
31
+ "antigravity"
32
+ ],
33
+ "author": "djrcx",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.0.0",
37
+ "chalk": "^5.3.0",
38
+ "cheerio": "^1.2.0",
39
+ "inquirer": "^9.2.15",
40
+ "ora": "^8.0.1",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/inquirer": "^9.0.7",
45
+ "@types/node": "^20.12.7",
46
+ "typescript": "^5.4.5"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ }
51
+ }