@codespar/mcp-rd-station 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @codespar/mcp-rd-station
2
+
3
+ > MCP server for **RD Station** — marketing automation and CRM
4
+
5
+ [![npm](https://img.shields.io/npm/v/@codespar/mcp-rd-station)](https://www.npmjs.com/package/@codespar/mcp-rd-station)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Quick Start
9
+
10
+ ### Claude Desktop
11
+
12
+ Add to `~/.config/claude/claude_desktop_config.json`:
13
+
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "rd-station": {
18
+ "command": "npx",
19
+ "args": ["-y", "@codespar/mcp-rd-station"],
20
+ "env": {
21
+ "RD_STATION_TOKEN": "your-token"
22
+ }
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ### Claude Code
29
+
30
+ ```bash
31
+ claude mcp add rd-station -- npx @codespar/mcp-rd-station
32
+ ```
33
+
34
+ ### Cursor / VS Code
35
+
36
+ Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
37
+
38
+ ```json
39
+ {
40
+ "servers": {
41
+ "rd-station": {
42
+ "command": "npx",
43
+ "args": ["-y", "@codespar/mcp-rd-station"],
44
+ "env": {
45
+ "RD_STATION_TOKEN": "your-token"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## Tools
53
+
54
+ | Tool | Description |
55
+ |------|-------------|
56
+ | `create_contact` | Create a contact in RD Station CRM |
57
+ | `update_contact` | Update a contact by UUID |
58
+ | `get_contact` | Get contact details by UUID or email |
59
+ | `list_contacts` | List contacts with pagination |
60
+ | `create_event` | Create a conversion event for a contact |
61
+ | `list_funnels` | List all sales funnels |
62
+ | `get_funnel` | Get funnel details with stages |
63
+ | `create_opportunity` | Create a sales opportunity in a funnel |
64
+
65
+ ## Authentication
66
+
67
+ RD Station uses a Bearer token for authentication.
68
+
69
+ ## Sandbox / Testing
70
+
71
+ RD Station provides an OAuth sandbox for testing. Use sandbox credentials during development.
72
+
73
+ ### Get your credentials
74
+
75
+ 1. Go to [RD Station Developer Portal](https://developers.rdstation.com)
76
+ 2. Create a developer account
77
+ 3. Register an OAuth application and obtain a token
78
+ 4. Set the `RD_STATION_TOKEN` environment variable
79
+
80
+ ## Environment Variables
81
+
82
+ | Variable | Required | Description |
83
+ |----------|----------|-------------|
84
+ | `RD_STATION_TOKEN` | Yes | Bearer token from RD Station |
85
+
86
+ ## Roadmap
87
+
88
+ ### v0.2 (planned)
89
+ - `list_deals` — List deals in the CRM pipeline
90
+ - `create_deal` — Create a new deal
91
+ - `update_deal` — Update deal details or stage
92
+ - `list_activities` — List activities for a contact or deal
93
+ - `create_task` — Create a task assigned to a user
94
+
95
+ ### v0.3 (planned)
96
+ - `custom_fields` — Manage custom fields for contacts and deals
97
+ - `automation_triggers` — Trigger marketing automation flows
98
+
99
+ Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [request a tool](https://github.com/codespar/mcp-dev-brasil/issues).
100
+
101
+ ## Links
102
+
103
+ - [RD Station Website](https://rdstation.com)
104
+ - [RD Station API Documentation](https://developers.rdstation.com)
105
+ - [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
106
+ - [Landing Page](https://codespar.dev/mcp)
107
+
108
+ ## Enterprise
109
+
110
+ Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
111
+
112
+ ## License
113
+
114
+ MIT
package/dist/index.js CHANGED
@@ -17,6 +17,8 @@
17
17
  */
18
18
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
19
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
20
22
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
21
23
  const TOKEN = process.env.RD_STATION_TOKEN || "";
22
24
  const BASE_URL = "https://api.rd.services";
@@ -200,11 +202,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
200
202
  }
201
203
  });
202
204
  async function main() {
203
- if (!TOKEN) {
204
- console.error("RD_STATION_TOKEN environment variable is required");
205
- process.exit(1);
205
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
206
+ const { default: express } = await import("express");
207
+ const { randomUUID } = await import("node:crypto");
208
+ const app = express();
209
+ app.use(express.json());
210
+ const transports = new Map();
211
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
212
+ app.post("/mcp", async (req, res) => {
213
+ const sid = req.headers["mcp-session-id"];
214
+ if (sid && transports.has(sid)) {
215
+ await transports.get(sid).handleRequest(req, res, req.body);
216
+ return;
217
+ }
218
+ if (!sid && isInitializeRequest(req.body)) {
219
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
220
+ t.onclose = () => { if (t.sessionId)
221
+ transports.delete(t.sessionId); };
222
+ const s = new Server({ name: "mcp-rd-station", version: "0.1.0" }, { capabilities: { tools: {} } });
223
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
224
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
225
+ await s.connect(t);
226
+ await t.handleRequest(req, res, req.body);
227
+ return;
228
+ }
229
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
230
+ });
231
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
232
+ await transports.get(sid).handleRequest(req, res);
233
+ else
234
+ res.status(400).send("Invalid session"); });
235
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
236
+ await transports.get(sid).handleRequest(req, res);
237
+ else
238
+ res.status(400).send("Invalid session"); });
239
+ const port = Number(process.env.MCP_PORT) || 3000;
240
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
241
+ }
242
+ else {
243
+ const transport = new StdioServerTransport();
244
+ await server.connect(transport);
206
245
  }
207
- const transport = new StdioServerTransport();
208
- await server.connect(transport);
209
246
  }
210
247
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codespar/mcp-rd-station",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for RD Station — contacts, events, funnels, opportunities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,5 +25,6 @@
25
25
  "crm",
26
26
  "marketing",
27
27
  "brazil"
28
- ]
28
+ ],
29
+ "mcpName": "io.github.codespar/mcp-rd-station"
29
30
  }
package/server.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.codespar/mcp-rd-station",
4
+ "description": "MCP server for RD Station — contacts, events, funnels, opportunities",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/communication/rd-station"
9
+ },
10
+ "version": "0.1.2",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-rd-station",
15
+ "version": "0.1.2",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "RD_STATION_TOKEN",
22
+ "description": "API key for rd-station",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": true
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ let listToolsHandler: Function;
4
+ let callToolHandler: Function;
5
+
6
+ vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
7
+ class FakeServer {
8
+ constructor() {}
9
+ setRequestHandler(schema: any, handler: Function) {
10
+ if (JSON.stringify(schema).includes("tools/list")) listToolsHandler = handler;
11
+ if (JSON.stringify(schema).includes("tools/call")) callToolHandler = handler;
12
+ }
13
+ connect() { return Promise.resolve(); }
14
+ }
15
+ return { Server: FakeServer };
16
+ });
17
+
18
+ vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: class {} }));
19
+
20
+ process.env.RD_STATION_TOKEN = "test-token";
21
+
22
+ const mockFetch = vi.fn();
23
+ global.fetch = mockFetch as any;
24
+
25
+ beforeEach(async () => {
26
+ vi.resetModules();
27
+ listToolsHandler = undefined as any;
28
+ callToolHandler = undefined as any;
29
+ mockFetch.mockReset();
30
+ global.fetch = mockFetch as any;
31
+ await import("../index.js");
32
+ });
33
+
34
+ describe("mcp-rd-station", () => {
35
+ it("should register 8 tools", async () => {
36
+ const result = await listToolsHandler();
37
+ expect(result.tools).toHaveLength(8);
38
+ });
39
+
40
+ it("should call correct API endpoint for create_contact", async () => {
41
+ mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ uuid: "c1" }) });
42
+
43
+ await callToolHandler({
44
+ params: { name: "create_contact", arguments: { name: "Test", email: "test@test.com" } },
45
+ });
46
+
47
+ const [url, opts] = mockFetch.mock.calls[0];
48
+ expect(url).toContain("api.rd.services/platform/contacts");
49
+ expect(opts.method).toBe("POST");
50
+ expect(opts.headers.Authorization).toBe("Bearer test-token");
51
+ });
52
+ });
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@
19
19
 
20
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
22
24
  import {
23
25
  CallToolRequestSchema,
24
26
  ListToolsRequestSchema,
@@ -211,12 +213,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
211
213
  });
212
214
 
213
215
  async function main() {
214
- if (!TOKEN) {
215
- console.error("RD_STATION_TOKEN environment variable is required");
216
- process.exit(1);
216
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
217
+ const { default: express } = await import("express");
218
+ const { randomUUID } = await import("node:crypto");
219
+ const app = express();
220
+ app.use(express.json());
221
+ const transports = new Map<string, StreamableHTTPServerTransport>();
222
+ app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
223
+ app.post("/mcp", async (req: any, res: any) => {
224
+ const sid = req.headers["mcp-session-id"] as string | undefined;
225
+ if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
226
+ if (!sid && isInitializeRequest(req.body)) {
227
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
228
+ t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
229
+ const s = new Server({ name: "mcp-rd-station", version: "0.1.0" }, { capabilities: { tools: {} } }); (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); await s.connect(t);
230
+ await t.handleRequest(req, res, req.body); return;
231
+ }
232
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
233
+ });
234
+ app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
235
+ app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
236
+ const port = Number(process.env.MCP_PORT) || 3000;
237
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
238
+ } else {
239
+ const transport = new StdioServerTransport();
240
+ await server.connect(transport);
217
241
  }
218
- const transport = new StdioServerTransport();
219
- await server.connect(transport);
220
242
  }
221
243
 
222
244
  main().catch(console.error);
package/tsconfig.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src",
8
8
  "strict": true,
9
+ "skipLibCheck": true,
9
10
  "esModuleInterop": true,
10
11
  "declaration": true
11
12
  },