@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 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
+ }
@@ -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
+ });
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Creates and configures the Updately MCP server with all tools registered.
4
+ */
5
+ export declare function createServer(): McpServer;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAccountTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerCampaignTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerContactTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerIcpTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerInboxTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerLeadTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerOutreachTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerSchedulerTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerSignalTools(server: McpServer): void;
@@ -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,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerStepTools(server: McpServer): void;
@@ -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
+ }