mcp-raindrop-server
Version:
A Model Context Protocol (MCP) server implementation for Raindrop.io API, providing bookmark management capabilities
168 lines (167 loc) • 5.61 kB
JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import dotenv from "dotenv";
import { z } from "zod";
dotenv.config();
const RAINDROP_API_BASE = "https://api.raindrop.io/rest/v1";
// 创建MCP服务器
const server = new McpServer({
name: "Raindrop.io MCP Server",
version: "1.0.0",
});
// APIリクエストヘルパー
async function makeRaindropRequest(endpoint, method = "GET", body) {
const token = process.env.RAINDROP_TOKEN;
if (!token) {
throw new Error("RAINDROP_TOKEN is not set");
}
const response = await fetch(`${RAINDROP_API_BASE}${endpoint}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
throw new Error(`Raindrop API error: ${response.statusText}`);
}
return response.json();
}
// 添加创建书签工具
server.tool("create-bookmark", "Create a new bookmark in Raindrop.io", {
url: z.string().url().describe("URL to bookmark"),
title: z.string().optional().describe("Title for the bookmark (optional)"),
tags: z
.array(z.string())
.optional()
.describe("Tags for the bookmark (optional)"),
collection: z
.number()
.optional()
.describe("Collection ID to save to (optional)"),
}, async ({ url, title, tags, collection }) => {
try {
const bookmark = await makeRaindropRequest("/raindrop", "POST", {
link: url,
title,
tags,
collection: collection || { $id: 0 },
});
return {
content: [
{
type: "text",
text: `Bookmark created successfully: ${bookmark.link}`,
},
],
};
}
catch (error) {
throw new Error(`Failed to create bookmark: ${error.message}`);
}
});
// 添加搜索书签工具
server.tool("search-bookmarks", "Search through your Raindrop.io bookmarks", {
query: z.string().describe("Search query"),
tags: z.array(z.string()).optional().describe("Filter by tags (optional)"),
page: z
.number()
.min(0)
.optional()
.describe("Page number (0-based, optional)"),
perpage: z
.number()
.min(1)
.max(50)
.optional()
.describe("Items per page (1-50, optional)"),
sort: z
.enum([
"-created",
"created",
"-last_update",
"last_update",
"-title",
"title",
"-domain",
"domain",
])
.optional()
.describe("Sort order (optional). Prefix with - for descending order."),
collection: z
.number()
.optional()
.describe("Collection ID to search in (optional, 0 for all collections)"),
word: z
.boolean()
.optional()
.describe("Whether to match exact words only (optional)"),
}, async ({ query, tags, page, perpage, sort, collection, word }) => {
try {
const searchParams = new URLSearchParams({
search: query,
...(tags && { tags: tags.join(",") }),
...(page !== undefined && { page: page.toString() }),
...(perpage !== undefined && { perpage: perpage.toString() }),
...(sort && { sort }),
...(word !== undefined && { word: word.toString() }),
});
const collectionId = collection ?? 0;
const results = await makeRaindropRequest(`/raindrops/${collectionId}?${searchParams}`);
const formattedResults = results.items
.map((item) => `
Title: ${item.title}
URL: ${item.link}
Tags: ${item.tags?.length ? item.tags.join(", ") : "No tags"}
Created: ${new Date(item.created).toLocaleString()}
Last Updated: ${new Date(item.lastUpdate).toLocaleString()}
---`)
.join("\n");
return {
content: [
{
type: "text",
text: results.items.length > 0
? `Found ${results.count} total bookmarks (showing ${results.items.length} on page ${page ?? 0 + 1}):\n${formattedResults}`
: "No bookmarks found matching your search.",
},
],
};
}
catch (error) {
throw new Error(`Search failed: ${error.message}`);
}
});
// 添加列出收藏夹工具
server.tool("list-collections", "List all your Raindrop.io collections", {}, async () => {
try {
const collections = await makeRaindropRequest("/collections");
const formattedCollections = collections.items
.map((item) => `
Name: ${item.title}
ID: ${item._id}
Count: ${item.count} bookmarks
Parent: ${item.parent?._id || "None"}
Created: ${new Date(item.created).toLocaleString()}
---`)
.join("\n");
return {
content: [
{
type: "text",
text: collections.items.length > 0
? `Found ${collections.items.length} collections:\n${formattedCollections}`
: "No collections found.",
},
],
};
}
catch (error) {
throw new Error(`Failed to list collections: ${error.message}`);
}
});
// Start the server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);