# 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:
--------------------------------------------------------------------------------
```
{
"semi": false
}
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# raindrop-mcp
An MCP server for [Raindrop.io](https://raindrop.io).
This project is sponsored by [ChatWise](https://chatwise.app), an all-in-one LLM chatbot with first-class MCP support.
## Usage
Create an access token on [Raindrop.io](https://app.raindrop.io/settings/integrations):
1. create an application
2. create a test token
3. copy the test token
JSON config for `raindrop-mcp` as `stdio` server:
```json
{
"mcpServers": {
"raindrop": {
"command": "npx",
"args": ["-y", "raindrop-mcp"],
"env": {
"RAINDROP_ACCESS_TOKEN": "<your-token>"
}
}
}
}
```
Alternatively you can run it as:
- sse server: `npx -y raindrop-mcp --sse`
- streamable http server: `npx -y raindrop-mcp --http`
## Capabilities
- Search bookmarks
- Create bookmarks
- Get all collections
## License
MIT.
```
--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import "./dist/index.js"
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
export const errorToToolResult = (error: unknown) => {
return {
content: [
{
type: "text" as const,
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { cac } from "cac"
import { version } from "../package.json"
import { startServer } from "./server"
const cli = cac(`fetch-mcp`)
cli
.command("[...args]", "Start server")
.option("--sse", "Use SSE transport")
.option("--http [endpoint]", "Use Streamable HTTP transport")
.action(async (args, flags) => {
await startServer(
flags.http
? {
type: "http",
endpoint: typeof flags.http === "string" ? flags.http : "/mcp",
}
: flags.sse
? { type: "sse" }
: { type: "stdio" }
)
})
cli.version(version)
cli.help()
cli.parse()
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "raindrop-mcp",
"description": "An MCP server for Raindrop.io by ChatWise",
"type": "module",
"version": "0.0.4",
"bin": "./cli.js",
"files": [
"dist",
"/cli.js"
],
"scripts": {
"build": "bun build src/index.ts --packages external --outdir dist --target node",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@types/bun": "latest",
"@types/js-yaml": "^4.0.9",
"@types/polka": "^0.5.7",
"typescript": "^5"
},
"dependencies": {
"@chatmcp/sdk": "^1.0.5",
"@modelcontextprotocol/sdk": "^1.9.0",
"cac": "^6.7.14",
"got": "^14.4.7",
"js-yaml": "^4.1.0",
"polka": "^0.5.2",
"zod": "^3.24.2"
}
}
```
--------------------------------------------------------------------------------
/src/raindrop.ts:
--------------------------------------------------------------------------------
```typescript
import { got, type Got } from "got"
import { z } from "zod"
export const SearchBookmarksSchema = {
search: z.string().describe("The search query"),
page: z.number().default(0).describe("The page number"),
perpage: z.number().default(50).describe("The number of bookmarks per page"),
sort: z.enum(["-created", "created"]).optional().describe(`Sort bookmarks`),
collection_id: z
.number()
.default(0)
.describe(
`The collection ID to filter bookmarks, 0 for all, -1 for unsorted, -99 for trash, others for specific collection`
),
}
type SearchBookmarksOptions = z.infer<z.ZodObject<typeof SearchBookmarksSchema>>
export const CreateBookmarksSchema = {
items: z.array(
z.object({
link: z.string().describe("The URL of this bookmark"),
title: z.string().optional().describe("The bookmark title"),
excerpt: z.string().optional().describe("The excerpt"),
})
),
}
type CreateBookmarksOptions = z.infer<z.ZodObject<typeof CreateBookmarksSchema>>
export class Raindrop {
got: Got
constructor(apiKey: string) {
this.got = got.extend({
prefixUrl: "https://api.raindrop.io/rest/v1",
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
}
async searchBookmarks({ collection_id, ...options }: SearchBookmarksOptions) {
const request = this.got.get(`raindrops/${collection_id}`, {
searchParams: options,
})
const [res, json] = await Promise.all([request, request.json()])
if (!res.ok) {
throw new Error(
`Failed to search bookmarks: ${res.statusCode}\n${res.body}`
)
}
return json
}
async createBookmarks(options: CreateBookmarksOptions) {
const request = this.got.post(`raindrops`, {
json: {
items: options.items.map((item) => {
return {
...item,
// let raindrop parse the title, description, and excerpt etc in the background
pleaseParse: {},
}
}),
},
})
const [res, json] = await Promise.all([request, request.json()])
if (!res.ok) {
throw new Error(
`Failed to create bookmarks: ${res.statusCode}\n${res.body}`
)
}
return json
}
async getCollections() {
const request = this.got.get("collections")
const [res, json] = await Promise.all([request, request.json()])
if (!res.ok) {
throw new Error(
`Failed to get collections: ${res.statusCode}\n${res.body}`
)
}
return json
}
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import Polka from "polka"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"
import { RestServerTransport } from "@chatmcp/sdk/server/rest.js"
import { version } from "../package.json"
import {
CreateBookmarksSchema,
Raindrop,
SearchBookmarksSchema,
} from "./raindrop"
import { dump } from "js-yaml"
import { errorToToolResult } from "./utils"
const server = new McpServer(
{
name: "raindrop-mcp",
version,
},
{
capabilities: {
logging: {},
tools: {},
},
}
)
if (!process.env.RAINDROP_ACCESS_TOKEN) {
throw new Error(`RAINDROP_ACCESS_TOKEN is not set`)
}
const raindrop = new Raindrop(process.env.RAINDROP_ACCESS_TOKEN)
server.tool(
"search_bookmarks",
"Search bookmarks from Raindrop.io",
SearchBookmarksSchema,
async (args) => {
try {
const res = await raindrop.searchBookmarks(args)
return {
content: [
{
type: "text",
text: dump(res),
},
],
}
} catch (error) {
return errorToToolResult(error)
}
}
)
server.tool(
"create_bookmarks",
"Create bookmarks on Raindrop.io",
CreateBookmarksSchema,
async (args) => {
try {
const res = await raindrop.createBookmarks(args)
return {
content: [
{
type: "text",
text: dump(res),
},
],
}
} catch (error) {
return errorToToolResult(error)
}
}
)
server.tool(
"get_collections",
"Get collections from Raindrop.io",
{},
async () => {
try {
const res = await raindrop.getCollections()
return {
content: [
{
type: "text",
text: dump(res),
},
],
}
} catch (error) {
return errorToToolResult(error)
}
}
)
const port = Number(process.env.PORT || "3000")
export async function startServer(
options:
| { type: "http"; endpoint: string }
| { type: "sse" }
| { type: "stdio" }
) {
if (options.type === "http") {
const transport = new RestServerTransport({
port,
endpoint: options.endpoint,
})
await server.connect(transport)
await transport.startServer()
} else if (options.type === "sse") {
const transports = new Map<string, SSEServerTransport>()
const app = Polka()
app.get("/sse", async (req, res) => {
console.log(req)
const transport = new SSEServerTransport("/messages", res)
transports.set(transport.sessionId, transport)
res.on("close", () => {
transports.delete(transport.sessionId)
})
await server.connect(transport)
})
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string
const transport = transports.get(sessionId)
if (transport) {
await transport.handlePostMessage(req, res)
} else {
res.status(400).send("No transport found for sessionId")
}
})
app.listen(port)
console.log(`sse server: http://localhost:${port}/sse`)
} else {
const transport = new StdioServerTransport()
await server.connect(transport)
}
}
```