@updately/mcp-server 1.0.1
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 +95 -0
- package/build/api-client.d.ts +16 -0
- package/build/api-client.js +74 -0
- package/build/index.d.ts +12 -0
- package/build/index.js +23 -0
- package/build/server.d.ts +5 -0
- package/build/server.js +32 -0
- package/build/tools/accounts.d.ts +2 -0
- package/build/tools/accounts.js +14 -0
- package/build/tools/campaigns.d.ts +2 -0
- package/build/tools/campaigns.js +39 -0
- package/build/tools/contacts.d.ts +2 -0
- package/build/tools/contacts.js +28 -0
- package/build/tools/icp.d.ts +2 -0
- package/build/tools/icp.js +77 -0
- package/build/tools/inbox.d.ts +2 -0
- package/build/tools/inbox.js +17 -0
- package/build/tools/leads.d.ts +2 -0
- package/build/tools/leads.js +20 -0
- package/build/tools/outreach.d.ts +2 -0
- package/build/tools/outreach.js +28 -0
- package/build/tools/scheduler.d.ts +2 -0
- package/build/tools/scheduler.js +21 -0
- package/build/tools/signals.d.ts +2 -0
- package/build/tools/signals.js +80 -0
- package/build/tools/steps.d.ts +2 -0
- package/build/tools/steps.js +28 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Updately MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for **Updately LinkedIn Outreach**. Lets AI assistants (Claude, Gemini, Cursor, etc.) manage your LinkedIn campaigns, contacts, signals, and ICP via natural language.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### 1. Install
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Generate an API Key
|
|
14
|
+
```bash
|
|
15
|
+
curl -X POST https://api.updately.ai/org/api-key/generate \
|
|
16
|
+
-H "Authorization: Bearer YOUR_ORG_ID" \
|
|
17
|
+
-H "Content-Type: application/json" \
|
|
18
|
+
-d '{"name": "MCP Server"}'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 3. Configure your IDE
|
|
22
|
+
|
|
23
|
+
**Claude Desktop / Cursor** — add to your MCP config:
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"updately": {
|
|
28
|
+
"command": "node",
|
|
29
|
+
"args": ["/absolute/path/to/updately-mcp-server/build/index.js"],
|
|
30
|
+
"env": {
|
|
31
|
+
"UPDATELY_API_KEY": "api_key_your-key-here",
|
|
32
|
+
"UPDATELY_API_URL": "https://api.updately.ai"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Available Tools (30)
|
|
40
|
+
|
|
41
|
+
### Campaigns
|
|
42
|
+
- `list_campaigns` — List all campaigns
|
|
43
|
+
- `get_campaign` — Get campaign details
|
|
44
|
+
- `create_campaign` — Create a campaign
|
|
45
|
+
- `update_campaign_status` — Activate/pause/stop
|
|
46
|
+
- `delete_campaign` — Delete a campaign
|
|
47
|
+
- `get_campaign_stats` — Get metrics (invited, accepted, replied)
|
|
48
|
+
|
|
49
|
+
### Workflow Steps
|
|
50
|
+
- `list_steps` — List steps in a campaign
|
|
51
|
+
- `upsert_step` — Create/update a step
|
|
52
|
+
- `delete_step` — Delete a step
|
|
53
|
+
|
|
54
|
+
### Contacts
|
|
55
|
+
- `list_contacts` — List enrolled contacts
|
|
56
|
+
- `enroll_contacts` — Add LinkedIn profiles
|
|
57
|
+
- `remove_contact` — Remove a contact
|
|
58
|
+
|
|
59
|
+
### Scheduler
|
|
60
|
+
- `run_campaign` — Trigger processing
|
|
61
|
+
- `process_contact` — Process one contact now
|
|
62
|
+
|
|
63
|
+
### Inbox
|
|
64
|
+
- `get_contact_messages` — Message history for a contact
|
|
65
|
+
- `send_message` — Send a LinkedIn message
|
|
66
|
+
|
|
67
|
+
### LinkedIn Accounts
|
|
68
|
+
- `list_accounts` — List connected accounts
|
|
69
|
+
- `get_account_usage` — Daily usage stats
|
|
70
|
+
|
|
71
|
+
### Lead Lists
|
|
72
|
+
- `list_lead_lists` — List all lists
|
|
73
|
+
- `get_leads_in_list` — Get leads in a list
|
|
74
|
+
- `create_lead_list` — Create a list
|
|
75
|
+
|
|
76
|
+
### Signals
|
|
77
|
+
- `list_signals` — List signals
|
|
78
|
+
- `get_signal` — Get signal details
|
|
79
|
+
- `create_signal` — Create a signal
|
|
80
|
+
- `update_signal` — Update config
|
|
81
|
+
- `run_signal` — Trigger a run
|
|
82
|
+
- `delete_signal` — Delete
|
|
83
|
+
|
|
84
|
+
### ICP (Ideal Customer Profile)
|
|
85
|
+
- `list_icps` — List ICPs
|
|
86
|
+
- `create_icp` — Create an ICP
|
|
87
|
+
- `update_icp` — Update an ICP
|
|
88
|
+
- `delete_icp` — Delete an ICP
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm run dev # Watch mode
|
|
94
|
+
npm run inspect # Test with MCP Inspector
|
|
95
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client wrapper for the Updately backend API.
|
|
3
|
+
* Sends all requests with the API key as Bearer token.
|
|
4
|
+
*/
|
|
5
|
+
export interface ApiResponse<T> {
|
|
6
|
+
success: boolean;
|
|
7
|
+
data: T;
|
|
8
|
+
message?: string;
|
|
9
|
+
count?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function apiGet<T>(path: string): Promise<ApiResponse<T>>;
|
|
12
|
+
export declare function apiPost<T>(path: string, body?: Record<string, unknown>): Promise<ApiResponse<T>>;
|
|
13
|
+
export declare function apiPut<T>(path: string, body?: Record<string, unknown>): Promise<ApiResponse<T>>;
|
|
14
|
+
export declare function apiDelete<T>(path: string): Promise<ApiResponse<T>>;
|
|
15
|
+
/** Format API response as MCP text content */
|
|
16
|
+
export declare function formatResponse(data: unknown): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client wrapper for the Updately backend API.
|
|
3
|
+
* Sends all requests with the API key as Bearer token.
|
|
4
|
+
*/
|
|
5
|
+
const API_URL = process.env.UPDATELY_API_URL || "https://api.updately.ai";
|
|
6
|
+
const API_KEY = process.env.UPDATELY_API_KEY || "";
|
|
7
|
+
if (!API_KEY) {
|
|
8
|
+
console.error("[updately-mcp] UPDATELY_API_KEY env var is required.");
|
|
9
|
+
}
|
|
10
|
+
export async function apiGet(path) {
|
|
11
|
+
const url = `${API_URL}${path}`;
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
method: "GET",
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
16
|
+
Accept: "application/json",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`GET ${path} failed: ${res.status} ${res.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
return (await res.json());
|
|
23
|
+
}
|
|
24
|
+
export async function apiPost(path, body) {
|
|
25
|
+
const url = `${API_URL}${path}`;
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
Accept: "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text().catch(() => "");
|
|
37
|
+
throw new Error(`POST ${path} failed: ${res.status} ${text}`);
|
|
38
|
+
}
|
|
39
|
+
return (await res.json());
|
|
40
|
+
}
|
|
41
|
+
export async function apiPut(path, body) {
|
|
42
|
+
const url = `${API_URL}${path}`;
|
|
43
|
+
const res = await fetch(url, {
|
|
44
|
+
method: "PUT",
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
Accept: "application/json",
|
|
49
|
+
},
|
|
50
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(`PUT ${path} failed: ${res.status} ${res.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
return (await res.json());
|
|
56
|
+
}
|
|
57
|
+
export async function apiDelete(path) {
|
|
58
|
+
const url = `${API_URL}${path}`;
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
method: "DELETE",
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
63
|
+
Accept: "application/json",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`DELETE ${path} failed: ${res.status} ${res.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
return (await res.json());
|
|
70
|
+
}
|
|
71
|
+
/** Format API response as MCP text content */
|
|
72
|
+
export function formatResponse(data) {
|
|
73
|
+
return JSON.stringify(data, null, 2);
|
|
74
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Updately MCP Server — entry point.
|
|
4
|
+
*
|
|
5
|
+
* Exposes LinkedIn outreach management tools (campaigns, contacts, inbox,
|
|
6
|
+
* signals, ICP, etc.) over the Model Context Protocol via stdio transport.
|
|
7
|
+
*
|
|
8
|
+
* Required env vars:
|
|
9
|
+
* UPDATELY_API_KEY — API key generated from the Updately settings
|
|
10
|
+
* UPDATELY_API_URL — Backend URL (default: https://api.updately.ai)
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/build/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Updately MCP Server — entry point.
|
|
4
|
+
*
|
|
5
|
+
* Exposes LinkedIn outreach management tools (campaigns, contacts, inbox,
|
|
6
|
+
* signals, ICP, etc.) over the Model Context Protocol via stdio transport.
|
|
7
|
+
*
|
|
8
|
+
* Required env vars:
|
|
9
|
+
* UPDATELY_API_KEY — API key generated from the Updately settings
|
|
10
|
+
* UPDATELY_API_URL — Backend URL (default: https://api.updately.ai)
|
|
11
|
+
*/
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { createServer } from "./server.js";
|
|
14
|
+
async function main() {
|
|
15
|
+
const server = createServer();
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
console.error("[updately-mcp] Server running on stdio");
|
|
19
|
+
}
|
|
20
|
+
main().catch((err) => {
|
|
21
|
+
console.error("[updately-mcp] Fatal error:", err);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
package/build/server.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerCampaignTools } from "./tools/campaigns.js";
|
|
3
|
+
import { registerStepTools } from "./tools/steps.js";
|
|
4
|
+
import { registerContactTools } from "./tools/contacts.js";
|
|
5
|
+
import { registerSchedulerTools } from "./tools/scheduler.js";
|
|
6
|
+
import { registerInboxTools } from "./tools/inbox.js";
|
|
7
|
+
import { registerAccountTools } from "./tools/accounts.js";
|
|
8
|
+
import { registerLeadTools } from "./tools/leads.js";
|
|
9
|
+
import { registerSignalTools } from "./tools/signals.js";
|
|
10
|
+
import { registerIcpTools } from "./tools/icp.js";
|
|
11
|
+
import { registerOutreachTools } from "./tools/outreach.js";
|
|
12
|
+
/**
|
|
13
|
+
* Creates and configures the Updately MCP server with all tools registered.
|
|
14
|
+
*/
|
|
15
|
+
export function createServer() {
|
|
16
|
+
const server = new McpServer({
|
|
17
|
+
name: "updately",
|
|
18
|
+
version: "1.0.0",
|
|
19
|
+
});
|
|
20
|
+
// Register all tool groups
|
|
21
|
+
registerCampaignTools(server);
|
|
22
|
+
registerStepTools(server);
|
|
23
|
+
registerContactTools(server);
|
|
24
|
+
registerSchedulerTools(server);
|
|
25
|
+
registerInboxTools(server);
|
|
26
|
+
registerAccountTools(server);
|
|
27
|
+
registerLeadTools(server);
|
|
28
|
+
registerSignalTools(server);
|
|
29
|
+
registerIcpTools(server);
|
|
30
|
+
registerOutreachTools(server);
|
|
31
|
+
return server;
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerAccountTools(server) {
|
|
4
|
+
server.tool("list_accounts", "List all connected LinkedIn accounts for the org", {}, async () => {
|
|
5
|
+
const res = await apiGet("/unipile/linkedin/accounts");
|
|
6
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
7
|
+
});
|
|
8
|
+
server.tool("get_account_usage", "Get daily usage stats for a LinkedIn account — invitations sent, DMs sent, limits", {
|
|
9
|
+
unipileAccountId: z.string().describe("The LinkedIn account ID"),
|
|
10
|
+
}, async ({ unipileAccountId }) => {
|
|
11
|
+
const res = await apiGet(`/unipile/linkedin/accounts/${unipileAccountId}/usage`);
|
|
12
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, apiPut, apiDelete, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerCampaignTools(server) {
|
|
4
|
+
server.tool("list_campaigns", "List all LinkedIn outreach campaigns for your org", {}, async () => {
|
|
5
|
+
const res = await apiGet("/campaigns");
|
|
6
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
7
|
+
});
|
|
8
|
+
server.tool("get_campaign", "Get details of a specific campaign by ID", { campaignId: z.string().describe("The campaign ID") }, async ({ campaignId }) => {
|
|
9
|
+
const res = await apiGet(`/campaigns/${campaignId}`);
|
|
10
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
11
|
+
});
|
|
12
|
+
server.tool("create_campaign", "Create a new LinkedIn outreach campaign", {
|
|
13
|
+
name: z.string().describe("Campaign name"),
|
|
14
|
+
unipileAccountId: z.string().describe("LinkedIn account ID to use"),
|
|
15
|
+
sourceListId: z.string().optional().describe("Lead list ID to import contacts from"),
|
|
16
|
+
}, async (args) => {
|
|
17
|
+
const res = await apiPost("/campaigns", args);
|
|
18
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
19
|
+
});
|
|
20
|
+
server.tool("update_campaign_status", "Activate, pause, or stop a campaign. Valid statuses: ACTIVE, PAUSED, STOPPED", {
|
|
21
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
22
|
+
status: z.enum(["ACTIVE", "PAUSED", "STOPPED"]).describe("New status"),
|
|
23
|
+
force: z.boolean().optional().describe("Force activation even if validation issues exist"),
|
|
24
|
+
}, async ({ campaignId, status, force }) => {
|
|
25
|
+
const body = { status };
|
|
26
|
+
if (force !== undefined)
|
|
27
|
+
body.force = force;
|
|
28
|
+
const res = await apiPut(`/campaigns/${campaignId}/status`, body);
|
|
29
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
30
|
+
});
|
|
31
|
+
server.tool("delete_campaign", "Delete a campaign permanently", { campaignId: z.string().describe("The campaign ID") }, async ({ campaignId }) => {
|
|
32
|
+
const res = await apiDelete(`/campaigns/${campaignId}`);
|
|
33
|
+
return { content: [{ type: "text", text: res.message || "Campaign deleted" }] };
|
|
34
|
+
});
|
|
35
|
+
server.tool("get_campaign_stats", "Get campaign metrics: total contacts, invited, accepted, replied, and acceptance/reply rates", { campaignId: z.string().describe("The campaign ID") }, async ({ campaignId }) => {
|
|
36
|
+
const res = await apiGet(`/campaigns/${campaignId}/stats`);
|
|
37
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, apiDelete, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerContactTools(server) {
|
|
4
|
+
server.tool("list_contacts", "List all contacts enrolled in a campaign, including their status (PENDING, IN_PROGRESS, COMPLETED, REPLIED, ERROR), chatId, and step progress", { campaignId: z.string().describe("The campaign ID") }, async ({ campaignId }) => {
|
|
5
|
+
const res = await apiGet(`/campaigns/${campaignId}/contacts`);
|
|
6
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
7
|
+
});
|
|
8
|
+
server.tool("enroll_contacts", "Enroll LinkedIn profiles into a campaign. Provide an array of LinkedIn profile URLs. Contacts are deduplicated by slug within the campaign.", {
|
|
9
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
10
|
+
linkedinProfileUrls: z
|
|
11
|
+
.array(z.string())
|
|
12
|
+
.describe("Array of LinkedIn profile URLs to enroll"),
|
|
13
|
+
}, async ({ campaignId, linkedinProfileUrls }) => {
|
|
14
|
+
const res = await apiPost(`/campaigns/${campaignId}/contacts`, {
|
|
15
|
+
linkedinProfileUrls,
|
|
16
|
+
});
|
|
17
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
18
|
+
});
|
|
19
|
+
server.tool("remove_contact", "Remove a contact from a campaign", {
|
|
20
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
21
|
+
contactId: z.string().describe("The contact ID to remove"),
|
|
22
|
+
}, async ({ campaignId, contactId }) => {
|
|
23
|
+
const res = await apiDelete(`/campaigns/${campaignId}/contacts/${contactId}`);
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: res.message || "Contact removed" }],
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, apiPut, apiDelete, formatResponse, } from "../api-client.js";
|
|
3
|
+
export function registerIcpTools(server) {
|
|
4
|
+
server.tool("list_icps", "List all ICP (Ideal Customer Profile) definitions for the org", {}, async () => {
|
|
5
|
+
const res = await apiGet("/icp");
|
|
6
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
7
|
+
});
|
|
8
|
+
server.tool("create_icp", "Create a new ICP (Ideal Customer Profile) to filter and score captured leads", {
|
|
9
|
+
name: z.string().describe("ICP name (e.g. 'VP Sales at SaaS')"),
|
|
10
|
+
targetJobTitles: z
|
|
11
|
+
.array(z.string())
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Job titles to match (e.g. ['VP of Sales', 'Head of Sales'])"),
|
|
14
|
+
targetIndustries: z
|
|
15
|
+
.array(z.string())
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Industries (e.g. ['SaaS', 'FinTech'])"),
|
|
18
|
+
companySizes: z
|
|
19
|
+
.array(z.string())
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Company sizes (e.g. ['51-200', '201-500'])"),
|
|
22
|
+
targetLocations: z
|
|
23
|
+
.array(z.string())
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Locations (e.g. ['United States', 'United Kingdom'])"),
|
|
26
|
+
companyTypes: z
|
|
27
|
+
.array(z.string())
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Company types (e.g. ['Startup', 'Enterprise'])"),
|
|
30
|
+
mandatoryKeywords: z
|
|
31
|
+
.array(z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Keywords that MUST appear in the lead's profile"),
|
|
34
|
+
excludeKeywords: z
|
|
35
|
+
.array(z.string())
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Keywords that should EXCLUDE a lead"),
|
|
38
|
+
excludeServiceProviders: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Exclude consultants/agencies"),
|
|
42
|
+
excludeOpenToWork: z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Exclude people with 'Open to Work' badge"),
|
|
46
|
+
additionalCriteria: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Free-text additional filtering criteria"),
|
|
50
|
+
leadMatchingMode: z
|
|
51
|
+
.enum(["DISCOVERY", "STRICT"])
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("DISCOVERY = broader match, STRICT = exact match"),
|
|
54
|
+
}, async (args) => {
|
|
55
|
+
const res = await apiPost("/icp", args);
|
|
56
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
57
|
+
});
|
|
58
|
+
server.tool("update_icp", "Update an existing ICP profile", {
|
|
59
|
+
icpId: z.string().describe("The ICP ID to update"),
|
|
60
|
+
name: z.string().optional().describe("Updated name"),
|
|
61
|
+
targetJobTitles: z.array(z.string()).optional().describe("Updated job titles"),
|
|
62
|
+
targetIndustries: z.array(z.string()).optional().describe("Updated industries"),
|
|
63
|
+
companySizes: z.array(z.string()).optional().describe("Updated company sizes"),
|
|
64
|
+
targetLocations: z.array(z.string()).optional().describe("Updated locations"),
|
|
65
|
+
mandatoryKeywords: z.array(z.string()).optional().describe("Updated mandatory keywords"),
|
|
66
|
+
excludeKeywords: z.array(z.string()).optional().describe("Updated exclude keywords"),
|
|
67
|
+
}, async ({ icpId, ...body }) => {
|
|
68
|
+
const res = await apiPut(`/icp/${icpId}`, body);
|
|
69
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
70
|
+
});
|
|
71
|
+
server.tool("delete_icp", "Delete an ICP profile", { icpId: z.string().describe("The ICP ID to delete") }, async ({ icpId }) => {
|
|
72
|
+
const res = await apiDelete(`/icp/${icpId}`);
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: res.message || "ICP deleted" }],
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerInboxTools(server) {
|
|
4
|
+
server.tool("get_contact_messages", "Get the full message history for a contact's LinkedIn chat. Use the chatId from list_contacts to identify the conversation.", {
|
|
5
|
+
chatId: z.string().describe("The chat ID (from the contact's chatId field)"),
|
|
6
|
+
}, async ({ chatId }) => {
|
|
7
|
+
const res = await apiGet(`/unipile/linkedin/inbox/chats/${chatId}/messages`);
|
|
8
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
9
|
+
});
|
|
10
|
+
server.tool("send_message", "Send a text message to a LinkedIn contact via their chat. First get the chatId from list_contacts.", {
|
|
11
|
+
chatId: z.string().describe("The chat ID"),
|
|
12
|
+
message: z.string().describe("The message text to send"),
|
|
13
|
+
}, async ({ chatId, message }) => {
|
|
14
|
+
const res = await apiPost(`/unipile/linkedin/inbox/chats/${chatId}/messages`, { message });
|
|
15
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerLeadTools(server) {
|
|
4
|
+
server.tool("list_lead_lists", "List all lead lists for the org. Each list contains captured LinkedIn leads from signals or manual imports.", {}, async () => {
|
|
5
|
+
const res = await apiGet("/leads/lists");
|
|
6
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
7
|
+
});
|
|
8
|
+
server.tool("get_leads_in_list", "Get all leads in a specific lead list. Returns LinkedIn profiles with name, title, company, intent, and ICP match info.", {
|
|
9
|
+
listId: z.string().describe("The lead list ID"),
|
|
10
|
+
}, async ({ listId }) => {
|
|
11
|
+
const res = await apiGet(`/leads/lists/${listId}/contacts`);
|
|
12
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
13
|
+
});
|
|
14
|
+
server.tool("create_lead_list", "Create a new empty lead list", {
|
|
15
|
+
name: z.string().describe("Name for the lead list"),
|
|
16
|
+
}, async ({ name }) => {
|
|
17
|
+
const res = await apiPost("/leads/lists", { name });
|
|
18
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiPost, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerOutreachTools(server) {
|
|
4
|
+
server.tool("reach_out", "Send a LinkedIn message or connection invitation to one or more profiles. " +
|
|
5
|
+
"Auto-detects connection status: sends a DM if already connected, " +
|
|
6
|
+
"or a connection invitation with the message as a note if not connected. " +
|
|
7
|
+
"Auto-picks the org's LinkedIn account if not specified.", {
|
|
8
|
+
linkedinProfileUrls: z
|
|
9
|
+
.array(z.string())
|
|
10
|
+
.describe("Array of LinkedIn profile URLs to reach out to (e.g. ['https://linkedin.com/in/johndoe'])"),
|
|
11
|
+
message: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Message text — used as DM (if connected) or invitation note (if not connected, max 300 chars)"),
|
|
14
|
+
unipileAccountId: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("LinkedIn account ID to send from (auto-picks the default if omitted)"),
|
|
18
|
+
}, async ({ linkedinProfileUrls, message, unipileAccountId }) => {
|
|
19
|
+
const body = {
|
|
20
|
+
linkedinProfileUrls,
|
|
21
|
+
message,
|
|
22
|
+
};
|
|
23
|
+
if (unipileAccountId)
|
|
24
|
+
body.unipileAccountId = unipileAccountId;
|
|
25
|
+
const res = await apiPost("/unipile/linkedin/outreach/reach-out", body);
|
|
26
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiPost, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerSchedulerTools(server) {
|
|
4
|
+
server.tool("run_campaign", "Trigger the campaign scheduler to process pending contacts. Executes the next workflow step for each eligible contact.", {
|
|
5
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
6
|
+
force: z.boolean().optional().describe("Force-process even if campaign is paused"),
|
|
7
|
+
}, async ({ campaignId, force }) => {
|
|
8
|
+
const body = {};
|
|
9
|
+
if (force !== undefined)
|
|
10
|
+
body.force = force;
|
|
11
|
+
const res = await apiPost(`/campaigns/${campaignId}/scheduler/run`, body);
|
|
12
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
13
|
+
});
|
|
14
|
+
server.tool("process_contact", "Process a single specific contact immediately — executes their next pending workflow step right now", {
|
|
15
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
16
|
+
contactId: z.string().describe("The contact ID to process"),
|
|
17
|
+
}, async ({ campaignId, contactId }) => {
|
|
18
|
+
const res = await apiPost(`/campaigns/${campaignId}/contacts/${contactId}/process`);
|
|
19
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, apiPut, apiDelete, formatResponse, } from "../api-client.js";
|
|
3
|
+
export function registerSignalTools(server) {
|
|
4
|
+
server.tool("list_signals", "List all configured signals. Signals auto-capture LinkedIn leads matching keyword groups or job searches.", {}, async () => {
|
|
5
|
+
const res = await apiGet("/signals");
|
|
6
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
7
|
+
});
|
|
8
|
+
server.tool("get_signal", "Get detailed configuration of a specific signal", { signalId: z.string().describe("The signal ID") }, async ({ signalId }) => {
|
|
9
|
+
const res = await apiGet(`/signals/${signalId}`);
|
|
10
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
11
|
+
});
|
|
12
|
+
server.tool("create_signal", "Create a new signal to auto-capture LinkedIn leads. Types: KEYWORD_GROUP (monitors posts matching keywords) or LINKEDIN_JOBS (scrapes job postings for hiring signals).", {
|
|
13
|
+
name: z.string().describe("Signal name"),
|
|
14
|
+
type: z
|
|
15
|
+
.enum(["KEYWORD_GROUP", "LINKEDIN_JOBS"])
|
|
16
|
+
.describe("Signal type"),
|
|
17
|
+
listIds: z
|
|
18
|
+
.array(z.string())
|
|
19
|
+
.describe("Lead list IDs to fan captured leads into"),
|
|
20
|
+
// KEYWORD_GROUP fields
|
|
21
|
+
keywordGroupIds: z
|
|
22
|
+
.array(z.string())
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Keyword group IDs to monitor (for KEYWORD_GROUP type)"),
|
|
25
|
+
keywordGroupNames: z
|
|
26
|
+
.array(z.string())
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Keyword group names (parallel to keywordGroupIds)"),
|
|
29
|
+
// LINKEDIN_JOBS fields
|
|
30
|
+
jobTitle: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Job title to search (for LINKEDIN_JOBS type, e.g. 'VP of Sales')"),
|
|
34
|
+
location: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Location filter (e.g. 'United Kingdom')"),
|
|
38
|
+
runFrequencyHours: z
|
|
39
|
+
.number()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("How often to run in hours (min 24, default 24)"),
|
|
42
|
+
icpId: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("ICP profile ID to filter captured leads"),
|
|
46
|
+
enabled: z.boolean().optional().describe("Enable signal (default: true)"),
|
|
47
|
+
}, async (args) => {
|
|
48
|
+
const res = await apiPost("/signals", args);
|
|
49
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
50
|
+
});
|
|
51
|
+
server.tool("update_signal", "Update an existing signal's configuration", {
|
|
52
|
+
signalId: z.string().describe("The signal ID"),
|
|
53
|
+
name: z.string().optional().describe("Updated name"),
|
|
54
|
+
listIds: z.array(z.string()).optional().describe("Updated list IDs"),
|
|
55
|
+
enabled: z.boolean().optional().describe("Enable/disable"),
|
|
56
|
+
jobTitle: z.string().optional().describe("Updated job title"),
|
|
57
|
+
location: z.string().optional().describe("Updated location"),
|
|
58
|
+
icpId: z.string().optional().describe("Updated ICP ID (use '__none__' to clear)"),
|
|
59
|
+
}, async ({ signalId, ...body }) => {
|
|
60
|
+
const res = await apiPut(`/signals/${signalId}`, body);
|
|
61
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
62
|
+
});
|
|
63
|
+
server.tool("run_signal", "Manually trigger a signal run right now. For LINKEDIN_JOBS, scrapes jobs and captures leads. For KEYWORD_GROUP, runs a backfill.", {
|
|
64
|
+
signalId: z.string().describe("The signal ID"),
|
|
65
|
+
force: z
|
|
66
|
+
.boolean()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Force-run even if signal is disabled"),
|
|
69
|
+
}, async ({ signalId, force }) => {
|
|
70
|
+
const path = `/signals/${signalId}/run${force ? "?force=true" : ""}`;
|
|
71
|
+
const res = await apiPost(path);
|
|
72
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
73
|
+
});
|
|
74
|
+
server.tool("delete_signal", "Delete a signal permanently", { signalId: z.string().describe("The signal ID") }, async ({ signalId }) => {
|
|
75
|
+
const res = await apiDelete(`/signals/${signalId}`);
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: res.message || "Signal deleted" }],
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, apiDelete, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerStepTools(server) {
|
|
4
|
+
server.tool("list_steps", "List all workflow steps in a campaign (INVITATION, MESSAGE, etc.)", { campaignId: z.string().describe("The campaign ID") }, async ({ campaignId }) => {
|
|
5
|
+
const res = await apiGet(`/campaigns/${campaignId}/steps`);
|
|
6
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
7
|
+
});
|
|
8
|
+
server.tool("upsert_step", "Create or update a campaign workflow step. Types: INVITATION (step 1), MESSAGE (follow-ups). Include templateId or inline message/subject.", {
|
|
9
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
10
|
+
stepOrder: z.number().describe("Step order (1-based)"),
|
|
11
|
+
type: z.enum(["INVITATION", "MESSAGE"]).describe("Step type"),
|
|
12
|
+
templateId: z.string().optional().describe("Template ID to use for this step"),
|
|
13
|
+
message: z.string().optional().describe("Inline message text (if no templateId)"),
|
|
14
|
+
subject: z.string().optional().describe("Subject line for InMail (optional)"),
|
|
15
|
+
delayHours: z.number().optional().describe("Hours to wait before executing this step (after previous step)"),
|
|
16
|
+
}, async (args) => {
|
|
17
|
+
const { campaignId, ...body } = args;
|
|
18
|
+
const res = await apiPost(`/campaigns/${campaignId}/steps`, body);
|
|
19
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
20
|
+
});
|
|
21
|
+
server.tool("delete_step", "Delete a workflow step from a campaign", {
|
|
22
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
23
|
+
stepOrder: z.number().describe("Step order to delete"),
|
|
24
|
+
}, async ({ campaignId, stepOrder }) => {
|
|
25
|
+
const res = await apiDelete(`/campaigns/${campaignId}/steps/${stepOrder}`);
|
|
26
|
+
return { content: [{ type: "text", text: res.message || "Step deleted" }] };
|
|
27
|
+
});
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@updately/mcp-server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "MCP server for Updately LinkedIn Outreach — exposes campaign management, contacts, inbox, signals, ICP and more as AI-callable tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"updately-mcp-server": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"start": "node build/index.js",
|
|
17
|
+
"inspect": "npx @modelcontextprotocol/inspector node build/index.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
21
|
+
"zod": "^3.24.4"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.15.0",
|
|
25
|
+
"typescript": "^5.8.3"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
}
|
|
30
|
+
}
|