#
tokens: 3775/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .prettierrc
├── bun.lock
├── cli.js
├── package.json
├── README.md
├── src
│   ├── index.ts
│   ├── raindrop.ts
│   ├── server.ts
│   └── utils.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
1 | {
2 |   "semi": false
3 | }
4 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # dependencies (bun install)
 2 | node_modules
 3 | 
 4 | # output
 5 | out
 6 | dist
 7 | *.tgz
 8 | 
 9 | # code coverage
10 | coverage
11 | *.lcov
12 | 
13 | # logs
14 | logs
15 | _.log
16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17 | 
18 | # dotenv environment variable files
19 | .env
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 | .env.local
24 | 
25 | # caches
26 | .eslintcache
27 | .cache
28 | *.tsbuildinfo
29 | 
30 | # IntelliJ based IDEs
31 | .idea
32 | 
33 | # Finder (MacOS) folder config
34 | .DS_Store
35 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # raindrop-mcp
 2 | 
 3 | An MCP server for [Raindrop.io](https://raindrop.io).
 4 | 
 5 | This project is sponsored by [ChatWise](https://chatwise.app), an all-in-one LLM chatbot with first-class MCP support.
 6 | 
 7 | ## Usage
 8 | 
 9 | Create an access token on [Raindrop.io](https://app.raindrop.io/settings/integrations):
10 | 
11 | 1. create an application
12 | 2. create a test token
13 | 3. copy the test token
14 | 
15 | JSON config for `raindrop-mcp` as `stdio` server:
16 | 
17 | ```json
18 | {
19 |   "mcpServers": {
20 |     "raindrop": {
21 |       "command": "npx",
22 |       "args": ["-y", "raindrop-mcp"],
23 |       "env": {
24 |         "RAINDROP_ACCESS_TOKEN": "<your-token>"
25 |       }
26 |     }
27 |   }
28 | }
29 | ```
30 | 
31 | Alternatively you can run it as:
32 | 
33 | - sse server: `npx -y raindrop-mcp --sse`
34 | - streamable http server: `npx -y raindrop-mcp --http`
35 | 
36 | ## Capabilities
37 | 
38 | - Search bookmarks
39 | - Create bookmarks
40 | - Get all collections
41 | 
42 | ## License
43 | 
44 | MIT.
45 | 
```

--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------

```javascript
1 | #!/usr/bin/env node
2 | import "./dist/index.js"
3 | 
```

--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export const errorToToolResult = (error: unknown) => {
 2 |   return {
 3 |     content: [
 4 |       {
 5 |         type: "text" as const,
 6 |         text: error instanceof Error ? error.message : String(error),
 7 |       },
 8 |     ],
 9 |     isError: true,
10 |   }
11 | }
12 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { cac } from "cac"
 2 | import { version } from "../package.json"
 3 | import { startServer } from "./server"
 4 | 
 5 | const cli = cac(`fetch-mcp`)
 6 | 
 7 | cli
 8 |   .command("[...args]", "Start server")
 9 |   .option("--sse", "Use SSE transport")
10 |   .option("--http [endpoint]", "Use Streamable HTTP transport")
11 |   .action(async (args, flags) => {
12 |     await startServer(
13 |       flags.http
14 |         ? {
15 |             type: "http",
16 |             endpoint: typeof flags.http === "string" ? flags.http : "/mcp",
17 |           }
18 |         : flags.sse
19 |         ? { type: "sse" }
20 |         : { type: "stdio" }
21 |     )
22 |   })
23 | 
24 | cli.version(version)
25 | cli.help()
26 | cli.parse()
27 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     // Environment setup & latest features
 4 |     "lib": ["ESNext"],
 5 |     "target": "ESNext",
 6 |     "module": "ESNext",
 7 |     "moduleDetection": "force",
 8 |     "jsx": "react-jsx",
 9 |     "allowJs": true,
10 | 
11 |     // Bundler mode
12 |     "moduleResolution": "bundler",
13 |     "allowImportingTsExtensions": true,
14 |     "verbatimModuleSyntax": true,
15 |     "noEmit": true,
16 | 
17 |     // Best practices
18 |     "strict": true,
19 |     "skipLibCheck": true,
20 |     "noFallthroughCasesInSwitch": true,
21 |     "noUncheckedIndexedAccess": true,
22 | 
23 |     // Some stricter flags (disabled by default)
24 |     "noUnusedLocals": false,
25 |     "noUnusedParameters": false,
26 |     "noPropertyAccessFromIndexSignature": false
27 |   }
28 | }
29 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "raindrop-mcp",
 3 |   "description": "An MCP server for Raindrop.io by ChatWise",
 4 |   "type": "module",
 5 |   "version": "0.0.4",
 6 |   "bin": "./cli.js",
 7 |   "files": [
 8 |     "dist",
 9 |     "/cli.js"
10 |   ],
11 |   "scripts": {
12 |     "build": "bun build src/index.ts --packages external --outdir dist --target node",
13 |     "prepublishOnly": "npm run build"
14 |   },
15 |   "devDependencies": {
16 |     "@types/bun": "latest",
17 |     "@types/js-yaml": "^4.0.9",
18 |     "@types/polka": "^0.5.7",
19 |     "typescript": "^5"
20 |   },
21 |   "dependencies": {
22 |     "@chatmcp/sdk": "^1.0.5",
23 |     "@modelcontextprotocol/sdk": "^1.9.0",
24 |     "cac": "^6.7.14",
25 |     "got": "^14.4.7",
26 |     "js-yaml": "^4.1.0",
27 |     "polka": "^0.5.2",
28 |     "zod": "^3.24.2"
29 |   }
30 | }
31 | 
```

--------------------------------------------------------------------------------
/src/raindrop.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { got, type Got } from "got"
 2 | import { z } from "zod"
 3 | 
 4 | export const SearchBookmarksSchema = {
 5 |   search: z.string().describe("The search query"),
 6 |   page: z.number().default(0).describe("The page number"),
 7 |   perpage: z.number().default(50).describe("The number of bookmarks per page"),
 8 |   sort: z.enum(["-created", "created"]).optional().describe(`Sort bookmarks`),
 9 |   collection_id: z
10 |     .number()
11 |     .default(0)
12 |     .describe(
13 |       `The collection ID to filter bookmarks, 0 for all, -1 for unsorted, -99 for trash, others for specific collection`
14 |     ),
15 | }
16 | 
17 | type SearchBookmarksOptions = z.infer<z.ZodObject<typeof SearchBookmarksSchema>>
18 | 
19 | export const CreateBookmarksSchema = {
20 |   items: z.array(
21 |     z.object({
22 |       link: z.string().describe("The URL of this bookmark"),
23 |       title: z.string().optional().describe("The bookmark title"),
24 |       excerpt: z.string().optional().describe("The excerpt"),
25 |     })
26 |   ),
27 | }
28 | 
29 | type CreateBookmarksOptions = z.infer<z.ZodObject<typeof CreateBookmarksSchema>>
30 | 
31 | export class Raindrop {
32 |   got: Got
33 | 
34 |   constructor(apiKey: string) {
35 |     this.got = got.extend({
36 |       prefixUrl: "https://api.raindrop.io/rest/v1",
37 |       headers: {
38 |         Authorization: `Bearer ${apiKey}`,
39 |       },
40 |     })
41 |   }
42 | 
43 |   async searchBookmarks({ collection_id, ...options }: SearchBookmarksOptions) {
44 |     const request = this.got.get(`raindrops/${collection_id}`, {
45 |       searchParams: options,
46 |     })
47 |     const [res, json] = await Promise.all([request, request.json()])
48 | 
49 |     if (!res.ok) {
50 |       throw new Error(
51 |         `Failed to search bookmarks: ${res.statusCode}\n${res.body}`
52 |       )
53 |     }
54 | 
55 |     return json
56 |   }
57 | 
58 |   async createBookmarks(options: CreateBookmarksOptions) {
59 |     const request = this.got.post(`raindrops`, {
60 |       json: {
61 |         items: options.items.map((item) => {
62 |           return {
63 |             ...item,
64 |             // let raindrop parse the title, description, and excerpt etc in the background
65 |             pleaseParse: {},
66 |           }
67 |         }),
68 |       },
69 |     })
70 | 
71 |     const [res, json] = await Promise.all([request, request.json()])
72 | 
73 |     if (!res.ok) {
74 |       throw new Error(
75 |         `Failed to create bookmarks: ${res.statusCode}\n${res.body}`
76 |       )
77 |     }
78 | 
79 |     return json
80 |   }
81 | 
82 |   async getCollections() {
83 |     const request = this.got.get("collections")
84 | 
85 |     const [res, json] = await Promise.all([request, request.json()])
86 | 
87 |     if (!res.ok) {
88 |       throw new Error(
89 |         `Failed to get collections: ${res.statusCode}\n${res.body}`
90 |       )
91 |     }
92 | 
93 |     return json
94 |   }
95 | }
96 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Polka from "polka"
  2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
  3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
  4 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"
  5 | import { RestServerTransport } from "@chatmcp/sdk/server/rest.js"
  6 | import { version } from "../package.json"
  7 | import {
  8 |   CreateBookmarksSchema,
  9 |   Raindrop,
 10 |   SearchBookmarksSchema,
 11 | } from "./raindrop"
 12 | import { dump } from "js-yaml"
 13 | import { errorToToolResult } from "./utils"
 14 | 
 15 | const server = new McpServer(
 16 |   {
 17 |     name: "raindrop-mcp",
 18 |     version,
 19 |   },
 20 |   {
 21 |     capabilities: {
 22 |       logging: {},
 23 |       tools: {},
 24 |     },
 25 |   }
 26 | )
 27 | 
 28 | if (!process.env.RAINDROP_ACCESS_TOKEN) {
 29 |   throw new Error(`RAINDROP_ACCESS_TOKEN is not set`)
 30 | }
 31 | 
 32 | const raindrop = new Raindrop(process.env.RAINDROP_ACCESS_TOKEN)
 33 | 
 34 | server.tool(
 35 |   "search_bookmarks",
 36 |   "Search bookmarks from Raindrop.io",
 37 |   SearchBookmarksSchema,
 38 |   async (args) => {
 39 |     try {
 40 |       const res = await raindrop.searchBookmarks(args)
 41 |       return {
 42 |         content: [
 43 |           {
 44 |             type: "text",
 45 |             text: dump(res),
 46 |           },
 47 |         ],
 48 |       }
 49 |     } catch (error) {
 50 |       return errorToToolResult(error)
 51 |     }
 52 |   }
 53 | )
 54 | 
 55 | server.tool(
 56 |   "create_bookmarks",
 57 |   "Create bookmarks on Raindrop.io",
 58 |   CreateBookmarksSchema,
 59 |   async (args) => {
 60 |     try {
 61 |       const res = await raindrop.createBookmarks(args)
 62 |       return {
 63 |         content: [
 64 |           {
 65 |             type: "text",
 66 |             text: dump(res),
 67 |           },
 68 |         ],
 69 |       }
 70 |     } catch (error) {
 71 |       return errorToToolResult(error)
 72 |     }
 73 |   }
 74 | )
 75 | 
 76 | server.tool(
 77 |   "get_collections",
 78 |   "Get collections from Raindrop.io",
 79 |   {},
 80 |   async () => {
 81 |     try {
 82 |       const res = await raindrop.getCollections()
 83 |       return {
 84 |         content: [
 85 |           {
 86 |             type: "text",
 87 |             text: dump(res),
 88 |           },
 89 |         ],
 90 |       }
 91 |     } catch (error) {
 92 |       return errorToToolResult(error)
 93 |     }
 94 |   }
 95 | )
 96 | const port = Number(process.env.PORT || "3000")
 97 | 
 98 | export async function startServer(
 99 |   options:
100 |     | { type: "http"; endpoint: string }
101 |     | { type: "sse" }
102 |     | { type: "stdio" }
103 | ) {
104 |   if (options.type === "http") {
105 |     const transport = new RestServerTransport({
106 |       port,
107 |       endpoint: options.endpoint,
108 |     })
109 |     await server.connect(transport)
110 | 
111 |     await transport.startServer()
112 |   } else if (options.type === "sse") {
113 |     const transports = new Map<string, SSEServerTransport>()
114 | 
115 |     const app = Polka()
116 | 
117 |     app.get("/sse", async (req, res) => {
118 |       console.log(req)
119 |       const transport = new SSEServerTransport("/messages", res)
120 |       transports.set(transport.sessionId, transport)
121 |       res.on("close", () => {
122 |         transports.delete(transport.sessionId)
123 |       })
124 |       await server.connect(transport)
125 |     })
126 | 
127 |     app.post("/messages", async (req, res) => {
128 |       const sessionId = req.query.sessionId as string
129 |       const transport = transports.get(sessionId)
130 |       if (transport) {
131 |         await transport.handlePostMessage(req, res)
132 |       } else {
133 |         res.status(400).send("No transport found for sessionId")
134 |       }
135 |     })
136 | 
137 |     app.listen(port)
138 |     console.log(`sse server: http://localhost:${port}/sse`)
139 |   } else {
140 |     const transport = new StdioServerTransport()
141 |     await server.connect(transport)
142 |   }
143 | }
144 | 
```