Raindrop.io MCP Server

by hiromitsusasaki
Verified
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequest, CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import dotenv from "dotenv"; import { z } from "zod"; dotenv.config(); const RAINDROP_API_BASE = "https://api.raindrop.io/rest/v1"; // バリデーションスキーマ const CreateBookmarkSchema = z.object({ url: z.string().url(), title: z.string().optional(), tags: z.array(z.string()).optional(), collection: z.number().optional(), }); const SearchBookmarksSchema = z.object({ query: z.string(), tags: z.array(z.string()).optional(), page: z.number().min(0).optional(), perpage: z.number().min(1).max(50).optional(), sort: z .enum([ "-created", "created", "-last_update", "last_update", "-title", "title", "-domain", "domain", ]) .optional(), collection: z.number().optional(), word: z.boolean().optional(), }); const server = new Server( { name: "raindrop-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // APIリクエストヘルパー async function makeRaindropRequest( endpoint: string, method: string = "GET", body?: any ) { 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.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "create-bookmark", description: "Create a new bookmark in Raindrop.io", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to bookmark", }, title: { type: "string", description: "Title for the bookmark (optional)", }, tags: { type: "array", items: { type: "string" }, description: "Tags for the bookmark (optional)", }, collection: { type: "number", description: "Collection ID to save to (optional)", }, }, required: ["url"], }, }, { name: "search-bookmarks", description: "Search through your Raindrop.io bookmarks", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query", }, tags: { type: "array", items: { type: "string" }, description: "Filter by tags (optional)", }, page: { type: "number", description: "Page number (0-based, optional)", }, perpage: { type: "number", description: "Items per page (1-50, optional)", }, sort: { type: "string", enum: [ "-created", "created", "-last_update", "last_update", "-title", "title", "-domain", "domain", ], description: "Sort order (optional). Prefix with - for descending order.", }, collection: { type: "number", description: "Collection ID to search in (optional, 0 for all collections)", }, word: { type: "boolean", description: "Whether to match exact words only (optional)", }, }, required: ["query"], }, }, { name: "list-collections", description: "List all your Raindrop.io collections", inputSchema: { type: "object", properties: {}, }, }, ], }; }); // ツール実行のハンドリング server.setRequestHandler( CallToolRequestSchema, async (request: CallToolRequest) => { const { name, arguments: args } = request.params; try { if (name === "create-bookmark") { const { url, title, tags, collection } = CreateBookmarkSchema.parse(args); const bookmark = await makeRaindropRequest("/raindrop", "POST", { link: url, title, tags, collection: collection || { $id: 0 }, }); return { content: [ { type: "text", text: `Bookmark created successfully: ${bookmark.link}`, }, ], }; } if (name === "search-bookmarks") { const { query, tags, page, perpage, sort, collection, word } = SearchBookmarksSchema.parse(args); 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: any) => ` 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.", }, ], }; } if (name === "list-collections") { const collections = await makeRaindropRequest("/collections"); const formattedCollections = collections.items .map( (item: any) => ` 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.", }, ], }; } throw new Error(`Unknown tool: ${name}`); } catch (error) { if (error instanceof z.ZodError) { throw new Error( `Invalid arguments: ${error.errors .map((e) => `${e.path.join(".")}: ${e.message}`) .join(", ")}` ); } throw error; } } ); // サーバー起動 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Raindrop MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });