@codespar/mcp-rd-station 0.1.0 → 0.2.0
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 +279 -8
- package/package.json +4 -3
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +52 -0
- package/src/index.ts +246 -8
- 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
|
@@ -2,21 +2,33 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MCP Server for RD Station — Brazilian CRM and marketing automation.
|
|
4
4
|
*
|
|
5
|
-
* Tools:
|
|
5
|
+
* Tools (18):
|
|
6
6
|
* - create_contact: Create a contact in RD Station
|
|
7
7
|
* - update_contact: Update a contact by UUID
|
|
8
|
+
* - upsert_contact: Upsert contact by email (Marketing API)
|
|
8
9
|
* - get_contact: Get contact details by UUID or email
|
|
9
10
|
* - list_contacts: List contacts with pagination
|
|
11
|
+
* - delete_contact: Delete a contact by UUID
|
|
10
12
|
* - create_event: Create a conversion event
|
|
11
13
|
* - list_funnels: List sales funnels
|
|
12
14
|
* - get_funnel: Get funnel details with stages
|
|
15
|
+
* - list_deal_stages: List deal stages of a pipeline
|
|
13
16
|
* - create_opportunity: Create a sales opportunity
|
|
17
|
+
* - update_deal: Update a deal/opportunity by ID
|
|
18
|
+
* - get_deal: Get a deal/opportunity by ID
|
|
19
|
+
* - list_deals: List deals with filters
|
|
20
|
+
* - list_segmentations: List contact segmentations
|
|
21
|
+
* - get_segmentation_contacts: List contacts of a segmentation
|
|
22
|
+
* - update_lead_scoring: Mark a contact as lead/opportunity (lead scoring)
|
|
23
|
+
* - create_webhook: Subscribe a webhook to RD Station events
|
|
14
24
|
*
|
|
15
25
|
* Environment:
|
|
16
26
|
* RD_STATION_TOKEN — Bearer token from https://app.rdstation.com/
|
|
17
27
|
*/
|
|
18
28
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
29
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
30
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
31
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
20
32
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
33
|
const TOKEN = process.env.RD_STATION_TOKEN || "";
|
|
22
34
|
const BASE_URL = "https://api.rd.services";
|
|
@@ -33,9 +45,20 @@ async function rdStationRequest(method, path, body) {
|
|
|
33
45
|
const err = await res.text();
|
|
34
46
|
throw new Error(`RD Station API ${res.status}: ${err}`);
|
|
35
47
|
}
|
|
36
|
-
return
|
|
48
|
+
// Some DELETE endpoints return 204 No Content
|
|
49
|
+
if (res.status === 204)
|
|
50
|
+
return { ok: true };
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
if (!text)
|
|
53
|
+
return { ok: true };
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { raw: text };
|
|
59
|
+
}
|
|
37
60
|
}
|
|
38
|
-
const server = new Server({ name: "mcp-rd-station", version: "0.
|
|
61
|
+
const server = new Server({ name: "mcp-rd-station", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
39
62
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
40
63
|
tools: [
|
|
41
64
|
{
|
|
@@ -76,6 +99,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
76
99
|
required: ["uuid"],
|
|
77
100
|
},
|
|
78
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: "upsert_contact",
|
|
104
|
+
description: "Upsert (create or update) a contact identified by email (Marketing API)",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
email: { type: "string", description: "Identifier email (used in path)" },
|
|
109
|
+
name: { type: "string", description: "Contact name" },
|
|
110
|
+
job_title: { type: "string", description: "Job title" },
|
|
111
|
+
mobile_phone: { type: "string", description: "Mobile phone" },
|
|
112
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags" },
|
|
113
|
+
cf_custom_fields: { type: "object", description: "Custom fields (key-value)" },
|
|
114
|
+
},
|
|
115
|
+
required: ["email"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
79
118
|
{
|
|
80
119
|
name: "get_contact",
|
|
81
120
|
description: "Get contact details by UUID or email",
|
|
@@ -99,6 +138,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
99
138
|
},
|
|
100
139
|
},
|
|
101
140
|
},
|
|
141
|
+
{
|
|
142
|
+
name: "delete_contact",
|
|
143
|
+
description: "Delete a contact by UUID",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
uuid: { type: "string", description: "Contact UUID" },
|
|
148
|
+
},
|
|
149
|
+
required: ["uuid"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
102
152
|
{
|
|
103
153
|
name: "create_event",
|
|
104
154
|
description: "Create a conversion event for a contact",
|
|
@@ -138,6 +188,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
138
188
|
required: ["id"],
|
|
139
189
|
},
|
|
140
190
|
},
|
|
191
|
+
{
|
|
192
|
+
name: "list_deal_stages",
|
|
193
|
+
description: "List deal stages of a pipeline (funnel)",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
deal_pipeline_id: { type: "string", description: "Pipeline (funnel) ID — optional filter" },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
141
201
|
{
|
|
142
202
|
name: "create_opportunity",
|
|
143
203
|
description: "Create a sales opportunity in a funnel",
|
|
@@ -153,6 +213,117 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
153
213
|
required: ["deal_stage_id", "name"],
|
|
154
214
|
},
|
|
155
215
|
},
|
|
216
|
+
{
|
|
217
|
+
name: "update_deal",
|
|
218
|
+
description: "Update a deal/opportunity by ID",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
id: { type: "string", description: "Deal ID" },
|
|
223
|
+
name: { type: "string", description: "Updated name" },
|
|
224
|
+
amount_total: { type: "number", description: "Total amount" },
|
|
225
|
+
deal_stage_id: { type: "string", description: "Move to this stage" },
|
|
226
|
+
win: { type: "boolean", description: "Mark as won" },
|
|
227
|
+
prediction_date: { type: "string", description: "Updated prediction date (YYYY-MM-DD)" },
|
|
228
|
+
},
|
|
229
|
+
required: ["id"],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "get_deal",
|
|
234
|
+
description: "Get a deal/opportunity by ID",
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: "object",
|
|
237
|
+
properties: {
|
|
238
|
+
id: { type: "string", description: "Deal ID" },
|
|
239
|
+
},
|
|
240
|
+
required: ["id"],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "list_deals",
|
|
245
|
+
description: "List deals with optional filters and pagination",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
page: { type: "number", description: "Page number (default 1)" },
|
|
250
|
+
limit: { type: "number", description: "Results per page (default 20)" },
|
|
251
|
+
deal_stage_id: { type: "string", description: "Filter by stage ID" },
|
|
252
|
+
deal_pipeline_id: { type: "string", description: "Filter by pipeline ID" },
|
|
253
|
+
user_id: { type: "string", description: "Filter by owner user ID" },
|
|
254
|
+
win: { type: "string", description: "Filter by win status: true | false | null" },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "list_segmentations",
|
|
260
|
+
description: "List contact segmentations",
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
page: { type: "number", description: "Page number" },
|
|
265
|
+
page_size: { type: "number", description: "Page size" },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "get_segmentation_contacts",
|
|
271
|
+
description: "List contacts inside a given segmentation",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
segmentation_id: { type: "string", description: "Segmentation ID" },
|
|
276
|
+
page: { type: "number", description: "Page number" },
|
|
277
|
+
page_size: { type: "number", description: "Page size" },
|
|
278
|
+
},
|
|
279
|
+
required: ["segmentation_id"],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "update_lead_scoring",
|
|
284
|
+
description: "Mark a contact as lead, qualified lead, or opportunity (lead scoring)",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
email: { type: "string", description: "Contact email" },
|
|
289
|
+
status: {
|
|
290
|
+
type: "string",
|
|
291
|
+
enum: ["opportunity", "qualified_lead", "lead", "client"],
|
|
292
|
+
description: "Lifecycle status to apply",
|
|
293
|
+
},
|
|
294
|
+
value: { type: "boolean", description: "Set/unset the status (default true)" },
|
|
295
|
+
},
|
|
296
|
+
required: ["email", "status"],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "create_webhook",
|
|
301
|
+
description: "Subscribe a webhook to RD Station events (WEBHOOK.CONVERTED / WEBHOOK.MARKED_OPPORTUNITY)",
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: {
|
|
305
|
+
entity_type: { type: "string", description: "Entity type (e.g. CONTACT)" },
|
|
306
|
+
event_type: {
|
|
307
|
+
type: "string",
|
|
308
|
+
enum: ["WEBHOOK.CONVERTED", "WEBHOOK.MARKED_OPPORTUNITY"],
|
|
309
|
+
description: "Event type to subscribe to",
|
|
310
|
+
},
|
|
311
|
+
event_identifiers: {
|
|
312
|
+
type: "array",
|
|
313
|
+
items: { type: "string" },
|
|
314
|
+
description: "Optional list of conversion identifiers",
|
|
315
|
+
},
|
|
316
|
+
url: { type: "string", description: "Destination URL" },
|
|
317
|
+
http_method: { type: "string", enum: ["POST", "GET"], description: "HTTP method (default POST)" },
|
|
318
|
+
include_relations: {
|
|
319
|
+
type: "array",
|
|
320
|
+
items: { type: "string" },
|
|
321
|
+
description: "Relations to include (e.g. COMPANY, CONTACT_FUNNEL)",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
required: ["entity_type", "event_type", "url"],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
156
327
|
],
|
|
157
328
|
}));
|
|
158
329
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -167,6 +338,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
167
338
|
delete body.uuid;
|
|
168
339
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("PATCH", `/platform/contacts/${uuid}`, body), null, 2) }] };
|
|
169
340
|
}
|
|
341
|
+
case "upsert_contact": {
|
|
342
|
+
const email = args?.email;
|
|
343
|
+
const body = { ...args };
|
|
344
|
+
delete body.email;
|
|
345
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("PATCH", `/platform/contacts/email:${email}`, body), null, 2) }] };
|
|
346
|
+
}
|
|
170
347
|
case "get_contact": {
|
|
171
348
|
if (args?.uuid) {
|
|
172
349
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/contacts/${args.uuid}`), null, 2) }] };
|
|
@@ -183,14 +360,73 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
183
360
|
params.set("query", String(args.query));
|
|
184
361
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/contacts?${params}`), null, 2) }] };
|
|
185
362
|
}
|
|
363
|
+
case "delete_contact":
|
|
364
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("DELETE", `/platform/contacts/${args?.uuid}`), null, 2) }] };
|
|
186
365
|
case "create_event":
|
|
187
366
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", "/platform/events", args), null, 2) }] };
|
|
188
367
|
case "list_funnels":
|
|
189
368
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", "/platform/deal_pipelines"), null, 2) }] };
|
|
190
369
|
case "get_funnel":
|
|
191
370
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deal_pipelines/${args?.id}`), null, 2) }] };
|
|
371
|
+
case "list_deal_stages": {
|
|
372
|
+
const params = new URLSearchParams();
|
|
373
|
+
if (args?.deal_pipeline_id)
|
|
374
|
+
params.set("deal_pipeline_id", String(args.deal_pipeline_id));
|
|
375
|
+
const qs = params.toString();
|
|
376
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deal_stages${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
377
|
+
}
|
|
192
378
|
case "create_opportunity":
|
|
193
379
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", "/platform/deals", args), null, 2) }] };
|
|
380
|
+
case "update_deal": {
|
|
381
|
+
const id = args?.id;
|
|
382
|
+
const body = { ...args };
|
|
383
|
+
delete body.id;
|
|
384
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("PUT", `/platform/deals/${id}`, body), null, 2) }] };
|
|
385
|
+
}
|
|
386
|
+
case "get_deal":
|
|
387
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deals/${args?.id}`), null, 2) }] };
|
|
388
|
+
case "list_deals": {
|
|
389
|
+
const params = new URLSearchParams();
|
|
390
|
+
if (args?.page)
|
|
391
|
+
params.set("page", String(args.page));
|
|
392
|
+
if (args?.limit)
|
|
393
|
+
params.set("limit", String(args.limit));
|
|
394
|
+
if (args?.deal_stage_id)
|
|
395
|
+
params.set("deal_stage_id", String(args.deal_stage_id));
|
|
396
|
+
if (args?.deal_pipeline_id)
|
|
397
|
+
params.set("deal_pipeline_id", String(args.deal_pipeline_id));
|
|
398
|
+
if (args?.user_id)
|
|
399
|
+
params.set("user_id", String(args.user_id));
|
|
400
|
+
if (args?.win !== undefined)
|
|
401
|
+
params.set("win", String(args.win));
|
|
402
|
+
const qs = params.toString();
|
|
403
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deals${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
404
|
+
}
|
|
405
|
+
case "list_segmentations": {
|
|
406
|
+
const params = new URLSearchParams();
|
|
407
|
+
if (args?.page)
|
|
408
|
+
params.set("page", String(args.page));
|
|
409
|
+
if (args?.page_size)
|
|
410
|
+
params.set("page_size", String(args.page_size));
|
|
411
|
+
const qs = params.toString();
|
|
412
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/segmentations${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
413
|
+
}
|
|
414
|
+
case "get_segmentation_contacts": {
|
|
415
|
+
const params = new URLSearchParams();
|
|
416
|
+
if (args?.page)
|
|
417
|
+
params.set("page", String(args.page));
|
|
418
|
+
if (args?.page_size)
|
|
419
|
+
params.set("page_size", String(args.page_size));
|
|
420
|
+
const qs = params.toString();
|
|
421
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/segmentations/${args?.segmentation_id}/contacts${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
422
|
+
}
|
|
423
|
+
case "update_lead_scoring": {
|
|
424
|
+
const status = String(args?.status ?? "lead");
|
|
425
|
+
const value = args?.value === undefined ? true : Boolean(args.value);
|
|
426
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", `/platform/contacts/email:${args?.email}/funnels/default`, { lifecycle_stage: status, value }), null, 2) }] };
|
|
427
|
+
}
|
|
428
|
+
case "create_webhook":
|
|
429
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", "/integrations/webhooks", args), null, 2) }] };
|
|
194
430
|
default:
|
|
195
431
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
196
432
|
}
|
|
@@ -200,11 +436,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
200
436
|
}
|
|
201
437
|
});
|
|
202
438
|
async function main() {
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
439
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
440
|
+
const { default: express } = await import("express");
|
|
441
|
+
const { randomUUID } = await import("node:crypto");
|
|
442
|
+
const app = express();
|
|
443
|
+
app.use(express.json());
|
|
444
|
+
const transports = new Map();
|
|
445
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
446
|
+
app.post("/mcp", async (req, res) => {
|
|
447
|
+
const sid = req.headers["mcp-session-id"];
|
|
448
|
+
if (sid && transports.has(sid)) {
|
|
449
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
453
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
454
|
+
t.onclose = () => { if (t.sessionId)
|
|
455
|
+
transports.delete(t.sessionId); };
|
|
456
|
+
const s = new Server({ name: "mcp-rd-station", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
457
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
458
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
459
|
+
await s.connect(t);
|
|
460
|
+
await t.handleRequest(req, res, req.body);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
464
|
+
});
|
|
465
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
466
|
+
await transports.get(sid).handleRequest(req, res);
|
|
467
|
+
else
|
|
468
|
+
res.status(400).send("Invalid session"); });
|
|
469
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
470
|
+
await transports.get(sid).handleRequest(req, res);
|
|
471
|
+
else
|
|
472
|
+
res.status(400).send("Invalid session"); });
|
|
473
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
474
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
const transport = new StdioServerTransport();
|
|
478
|
+
await server.connect(transport);
|
|
206
479
|
}
|
|
207
|
-
const transport = new StdioServerTransport();
|
|
208
|
-
await server.connect(transport);
|
|
209
480
|
}
|
|
210
481
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespar/mcp-rd-station",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for RD Station — contacts, events, funnels,
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for RD Station — contacts, events, funnels, deals, segmentations, lead scoring, webhooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -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, deals, segmentations, lead scoring, webhooks",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/communication/rd-station"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.2.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-rd-station",
|
|
15
|
+
"version": "0.2.0",
|
|
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
|
@@ -3,15 +3,25 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* MCP Server for RD Station — Brazilian CRM and marketing automation.
|
|
5
5
|
*
|
|
6
|
-
* Tools:
|
|
6
|
+
* Tools (18):
|
|
7
7
|
* - create_contact: Create a contact in RD Station
|
|
8
8
|
* - update_contact: Update a contact by UUID
|
|
9
|
+
* - upsert_contact: Upsert contact by email (Marketing API)
|
|
9
10
|
* - get_contact: Get contact details by UUID or email
|
|
10
11
|
* - list_contacts: List contacts with pagination
|
|
12
|
+
* - delete_contact: Delete a contact by UUID
|
|
11
13
|
* - create_event: Create a conversion event
|
|
12
14
|
* - list_funnels: List sales funnels
|
|
13
15
|
* - get_funnel: Get funnel details with stages
|
|
16
|
+
* - list_deal_stages: List deal stages of a pipeline
|
|
14
17
|
* - create_opportunity: Create a sales opportunity
|
|
18
|
+
* - update_deal: Update a deal/opportunity by ID
|
|
19
|
+
* - get_deal: Get a deal/opportunity by ID
|
|
20
|
+
* - list_deals: List deals with filters
|
|
21
|
+
* - list_segmentations: List contact segmentations
|
|
22
|
+
* - get_segmentation_contacts: List contacts of a segmentation
|
|
23
|
+
* - update_lead_scoring: Mark a contact as lead/opportunity (lead scoring)
|
|
24
|
+
* - create_webhook: Subscribe a webhook to RD Station events
|
|
15
25
|
*
|
|
16
26
|
* Environment:
|
|
17
27
|
* RD_STATION_TOKEN — Bearer token from https://app.rdstation.com/
|
|
@@ -19,6 +29,8 @@
|
|
|
19
29
|
|
|
20
30
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
31
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
32
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
33
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
22
34
|
import {
|
|
23
35
|
CallToolRequestSchema,
|
|
24
36
|
ListToolsRequestSchema,
|
|
@@ -40,11 +52,15 @@ async function rdStationRequest(method: string, path: string, body?: unknown): P
|
|
|
40
52
|
const err = await res.text();
|
|
41
53
|
throw new Error(`RD Station API ${res.status}: ${err}`);
|
|
42
54
|
}
|
|
43
|
-
return
|
|
55
|
+
// Some DELETE endpoints return 204 No Content
|
|
56
|
+
if (res.status === 204) return { ok: true };
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
if (!text) return { ok: true };
|
|
59
|
+
try { return JSON.parse(text); } catch { return { raw: text }; }
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
const server = new Server(
|
|
47
|
-
{ name: "mcp-rd-station", version: "0.
|
|
63
|
+
{ name: "mcp-rd-station", version: "0.2.0" },
|
|
48
64
|
{ capabilities: { tools: {} } }
|
|
49
65
|
);
|
|
50
66
|
|
|
@@ -88,6 +104,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
88
104
|
required: ["uuid"],
|
|
89
105
|
},
|
|
90
106
|
},
|
|
107
|
+
{
|
|
108
|
+
name: "upsert_contact",
|
|
109
|
+
description: "Upsert (create or update) a contact identified by email (Marketing API)",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
email: { type: "string", description: "Identifier email (used in path)" },
|
|
114
|
+
name: { type: "string", description: "Contact name" },
|
|
115
|
+
job_title: { type: "string", description: "Job title" },
|
|
116
|
+
mobile_phone: { type: "string", description: "Mobile phone" },
|
|
117
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags" },
|
|
118
|
+
cf_custom_fields: { type: "object", description: "Custom fields (key-value)" },
|
|
119
|
+
},
|
|
120
|
+
required: ["email"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
91
123
|
{
|
|
92
124
|
name: "get_contact",
|
|
93
125
|
description: "Get contact details by UUID or email",
|
|
@@ -111,6 +143,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
111
143
|
},
|
|
112
144
|
},
|
|
113
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: "delete_contact",
|
|
148
|
+
description: "Delete a contact by UUID",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
uuid: { type: "string", description: "Contact UUID" },
|
|
153
|
+
},
|
|
154
|
+
required: ["uuid"],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
114
157
|
{
|
|
115
158
|
name: "create_event",
|
|
116
159
|
description: "Create a conversion event for a contact",
|
|
@@ -150,6 +193,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
150
193
|
required: ["id"],
|
|
151
194
|
},
|
|
152
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: "list_deal_stages",
|
|
198
|
+
description: "List deal stages of a pipeline (funnel)",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
deal_pipeline_id: { type: "string", description: "Pipeline (funnel) ID — optional filter" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
153
206
|
{
|
|
154
207
|
name: "create_opportunity",
|
|
155
208
|
description: "Create a sales opportunity in a funnel",
|
|
@@ -165,6 +218,117 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
165
218
|
required: ["deal_stage_id", "name"],
|
|
166
219
|
},
|
|
167
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: "update_deal",
|
|
223
|
+
description: "Update a deal/opportunity by ID",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
id: { type: "string", description: "Deal ID" },
|
|
228
|
+
name: { type: "string", description: "Updated name" },
|
|
229
|
+
amount_total: { type: "number", description: "Total amount" },
|
|
230
|
+
deal_stage_id: { type: "string", description: "Move to this stage" },
|
|
231
|
+
win: { type: "boolean", description: "Mark as won" },
|
|
232
|
+
prediction_date: { type: "string", description: "Updated prediction date (YYYY-MM-DD)" },
|
|
233
|
+
},
|
|
234
|
+
required: ["id"],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "get_deal",
|
|
239
|
+
description: "Get a deal/opportunity by ID",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
id: { type: "string", description: "Deal ID" },
|
|
244
|
+
},
|
|
245
|
+
required: ["id"],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "list_deals",
|
|
250
|
+
description: "List deals with optional filters and pagination",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
page: { type: "number", description: "Page number (default 1)" },
|
|
255
|
+
limit: { type: "number", description: "Results per page (default 20)" },
|
|
256
|
+
deal_stage_id: { type: "string", description: "Filter by stage ID" },
|
|
257
|
+
deal_pipeline_id: { type: "string", description: "Filter by pipeline ID" },
|
|
258
|
+
user_id: { type: "string", description: "Filter by owner user ID" },
|
|
259
|
+
win: { type: "string", description: "Filter by win status: true | false | null" },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "list_segmentations",
|
|
265
|
+
description: "List contact segmentations",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
page: { type: "number", description: "Page number" },
|
|
270
|
+
page_size: { type: "number", description: "Page size" },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "get_segmentation_contacts",
|
|
276
|
+
description: "List contacts inside a given segmentation",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: {
|
|
280
|
+
segmentation_id: { type: "string", description: "Segmentation ID" },
|
|
281
|
+
page: { type: "number", description: "Page number" },
|
|
282
|
+
page_size: { type: "number", description: "Page size" },
|
|
283
|
+
},
|
|
284
|
+
required: ["segmentation_id"],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "update_lead_scoring",
|
|
289
|
+
description: "Mark a contact as lead, qualified lead, or opportunity (lead scoring)",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
email: { type: "string", description: "Contact email" },
|
|
294
|
+
status: {
|
|
295
|
+
type: "string",
|
|
296
|
+
enum: ["opportunity", "qualified_lead", "lead", "client"],
|
|
297
|
+
description: "Lifecycle status to apply",
|
|
298
|
+
},
|
|
299
|
+
value: { type: "boolean", description: "Set/unset the status (default true)" },
|
|
300
|
+
},
|
|
301
|
+
required: ["email", "status"],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "create_webhook",
|
|
306
|
+
description: "Subscribe a webhook to RD Station events (WEBHOOK.CONVERTED / WEBHOOK.MARKED_OPPORTUNITY)",
|
|
307
|
+
inputSchema: {
|
|
308
|
+
type: "object",
|
|
309
|
+
properties: {
|
|
310
|
+
entity_type: { type: "string", description: "Entity type (e.g. CONTACT)" },
|
|
311
|
+
event_type: {
|
|
312
|
+
type: "string",
|
|
313
|
+
enum: ["WEBHOOK.CONVERTED", "WEBHOOK.MARKED_OPPORTUNITY"],
|
|
314
|
+
description: "Event type to subscribe to",
|
|
315
|
+
},
|
|
316
|
+
event_identifiers: {
|
|
317
|
+
type: "array",
|
|
318
|
+
items: { type: "string" },
|
|
319
|
+
description: "Optional list of conversion identifiers",
|
|
320
|
+
},
|
|
321
|
+
url: { type: "string", description: "Destination URL" },
|
|
322
|
+
http_method: { type: "string", enum: ["POST", "GET"], description: "HTTP method (default POST)" },
|
|
323
|
+
include_relations: {
|
|
324
|
+
type: "array",
|
|
325
|
+
items: { type: "string" },
|
|
326
|
+
description: "Relations to include (e.g. COMPANY, CONTACT_FUNNEL)",
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
required: ["entity_type", "event_type", "url"],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
168
332
|
],
|
|
169
333
|
}));
|
|
170
334
|
|
|
@@ -181,6 +345,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
181
345
|
delete body.uuid;
|
|
182
346
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("PATCH", `/platform/contacts/${uuid}`, body), null, 2) }] };
|
|
183
347
|
}
|
|
348
|
+
case "upsert_contact": {
|
|
349
|
+
const email = args?.email;
|
|
350
|
+
const body = { ...args } as Record<string, unknown>;
|
|
351
|
+
delete body.email;
|
|
352
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("PATCH", `/platform/contacts/email:${email}`, body), null, 2) }] };
|
|
353
|
+
}
|
|
184
354
|
case "get_contact": {
|
|
185
355
|
if (args?.uuid) {
|
|
186
356
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/contacts/${args.uuid}`), null, 2) }] };
|
|
@@ -194,14 +364,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
194
364
|
if (args?.query) params.set("query", String(args.query));
|
|
195
365
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/contacts?${params}`), null, 2) }] };
|
|
196
366
|
}
|
|
367
|
+
case "delete_contact":
|
|
368
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("DELETE", `/platform/contacts/${args?.uuid}`), null, 2) }] };
|
|
197
369
|
case "create_event":
|
|
198
370
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", "/platform/events", args), null, 2) }] };
|
|
199
371
|
case "list_funnels":
|
|
200
372
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", "/platform/deal_pipelines"), null, 2) }] };
|
|
201
373
|
case "get_funnel":
|
|
202
374
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deal_pipelines/${args?.id}`), null, 2) }] };
|
|
375
|
+
case "list_deal_stages": {
|
|
376
|
+
const params = new URLSearchParams();
|
|
377
|
+
if (args?.deal_pipeline_id) params.set("deal_pipeline_id", String(args.deal_pipeline_id));
|
|
378
|
+
const qs = params.toString();
|
|
379
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deal_stages${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
380
|
+
}
|
|
203
381
|
case "create_opportunity":
|
|
204
382
|
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", "/platform/deals", args), null, 2) }] };
|
|
383
|
+
case "update_deal": {
|
|
384
|
+
const id = args?.id;
|
|
385
|
+
const body = { ...args } as Record<string, unknown>;
|
|
386
|
+
delete body.id;
|
|
387
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("PUT", `/platform/deals/${id}`, body), null, 2) }] };
|
|
388
|
+
}
|
|
389
|
+
case "get_deal":
|
|
390
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deals/${args?.id}`), null, 2) }] };
|
|
391
|
+
case "list_deals": {
|
|
392
|
+
const params = new URLSearchParams();
|
|
393
|
+
if (args?.page) params.set("page", String(args.page));
|
|
394
|
+
if (args?.limit) params.set("limit", String(args.limit));
|
|
395
|
+
if (args?.deal_stage_id) params.set("deal_stage_id", String(args.deal_stage_id));
|
|
396
|
+
if (args?.deal_pipeline_id) params.set("deal_pipeline_id", String(args.deal_pipeline_id));
|
|
397
|
+
if (args?.user_id) params.set("user_id", String(args.user_id));
|
|
398
|
+
if (args?.win !== undefined) params.set("win", String(args.win));
|
|
399
|
+
const qs = params.toString();
|
|
400
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/deals${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
401
|
+
}
|
|
402
|
+
case "list_segmentations": {
|
|
403
|
+
const params = new URLSearchParams();
|
|
404
|
+
if (args?.page) params.set("page", String(args.page));
|
|
405
|
+
if (args?.page_size) params.set("page_size", String(args.page_size));
|
|
406
|
+
const qs = params.toString();
|
|
407
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/segmentations${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
408
|
+
}
|
|
409
|
+
case "get_segmentation_contacts": {
|
|
410
|
+
const params = new URLSearchParams();
|
|
411
|
+
if (args?.page) params.set("page", String(args.page));
|
|
412
|
+
if (args?.page_size) params.set("page_size", String(args.page_size));
|
|
413
|
+
const qs = params.toString();
|
|
414
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("GET", `/platform/segmentations/${args?.segmentation_id}/contacts${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
415
|
+
}
|
|
416
|
+
case "update_lead_scoring": {
|
|
417
|
+
const status = String(args?.status ?? "lead");
|
|
418
|
+
const value = args?.value === undefined ? true : Boolean(args.value);
|
|
419
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", `/platform/contacts/email:${args?.email}/funnels/default`, { lifecycle_stage: status, value }), null, 2) }] };
|
|
420
|
+
}
|
|
421
|
+
case "create_webhook":
|
|
422
|
+
return { content: [{ type: "text", text: JSON.stringify(await rdStationRequest("POST", "/integrations/webhooks", args), null, 2) }] };
|
|
205
423
|
default:
|
|
206
424
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
207
425
|
}
|
|
@@ -211,12 +429,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
211
429
|
});
|
|
212
430
|
|
|
213
431
|
async function main() {
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
432
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
433
|
+
const { default: express } = await import("express");
|
|
434
|
+
const { randomUUID } = await import("node:crypto");
|
|
435
|
+
const app = express();
|
|
436
|
+
app.use(express.json());
|
|
437
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
438
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
439
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
440
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
441
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
442
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
443
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
444
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
445
|
+
const s = new Server({ name: "mcp-rd-station", version: "0.2.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);
|
|
446
|
+
await t.handleRequest(req, res, req.body); return;
|
|
447
|
+
}
|
|
448
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
449
|
+
});
|
|
450
|
+
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"); });
|
|
451
|
+
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"); });
|
|
452
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
453
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
454
|
+
} else {
|
|
455
|
+
const transport = new StdioServerTransport();
|
|
456
|
+
await server.connect(transport);
|
|
217
457
|
}
|
|
218
|
-
const transport = new StdioServerTransport();
|
|
219
|
-
await server.connect(transport);
|
|
220
458
|
}
|
|
221
459
|
|
|
222
460
|
main().catch(console.error);
|