@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 +44 -0
- package/dist/config.js +75 -0
- package/dist/index.js +437 -0
- package/dist/setup.js +158 -0
- package/package.json +51 -0
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
|
+
}
|