#
tokens: 2681/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# 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)
  }
}

```