@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 +114 -0
- package/dist/index.js +42 -5
- package/package.json +3 -2
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +52 -0
- package/src/index.ts +27 -5
- package/tsconfig.json +1 -0
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
|
+
[](https://www.npmjs.com/package/@codespar/mcp-rd-station)
|
|
6
|
+
[](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 (
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
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 (
|
|
215
|
-
|
|
216
|
-
|
|
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);
|