@updately/mcp-server 1.0.2 → 1.1.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
CHANGED
|
@@ -36,7 +36,7 @@ curl -X POST https://api.updately.ai/org/api-key/generate \
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
## Available Tools (
|
|
39
|
+
## Available Tools (37)
|
|
40
40
|
|
|
41
41
|
### Campaigns
|
|
42
42
|
- `list_campaigns` — List all campaigns
|
|
@@ -56,6 +56,19 @@ curl -X POST https://api.updately.ai/org/api-key/generate \
|
|
|
56
56
|
- `enroll_contacts` — Add LinkedIn profiles
|
|
57
57
|
- `remove_contact` — Remove a contact
|
|
58
58
|
|
|
59
|
+
### Direct Outreach
|
|
60
|
+
- `reach_out` — Send a LinkedIn message or connection invitation to one or more profiles. Auto-detects connection status: sends a DM if already connected, or a connection invite with the message as note if not. Auto-picks the org's LinkedIn account if not specified.
|
|
61
|
+
|
|
62
|
+
### Lead Search
|
|
63
|
+
- `search_leads` — Search for LinkedIn leads using natural language (e.g. "DevOps in SF, companies with 100+ employees", "VP of Sales at SaaS startups in NYC"). Returns matching profiles with name, headline, company, location, and LinkedIn URL.
|
|
64
|
+
- `confirm_lead_action` — Confirm and execute a suggested action from search_leads (e.g. create a signal, ICP, or campaign from the results)
|
|
65
|
+
|
|
66
|
+
### Social Listening
|
|
67
|
+
- `list_keyword_groups` — List all keyword groups configured for social listening
|
|
68
|
+
- `fetch_posts` — Fetch social-media posts matching keyword groups and/or platforms (Reddit, Twitter, LinkedIn, Quora, HackerNews, GitHub, StackOverflow). Returns scored, ranked posts with engagement metrics, sentiment, priority, and lead-type tags.
|
|
69
|
+
- `save_post` — Save/bookmark a post for follow-up
|
|
70
|
+
- `get_saved_posts` — List saved posts
|
|
71
|
+
|
|
59
72
|
### Scheduler
|
|
60
73
|
- `run_campaign` — Trigger processing
|
|
61
74
|
- `process_contact` — Process one contact now
|
|
@@ -76,7 +89,7 @@ curl -X POST https://api.updately.ai/org/api-key/generate \
|
|
|
76
89
|
### Signals
|
|
77
90
|
- `list_signals` — List signals
|
|
78
91
|
- `get_signal` — Get signal details
|
|
79
|
-
- `create_signal` — Create a signal
|
|
92
|
+
- `create_signal` — Create a signal (KEYWORD_GROUP, LINKEDIN_JOBS, PROFILE_SEARCH, or NETWORK)
|
|
80
93
|
- `update_signal` — Update config
|
|
81
94
|
- `run_signal` — Trigger a run
|
|
82
95
|
- `delete_signal` — Delete
|
package/build/server.js
CHANGED
|
@@ -9,6 +9,8 @@ import { registerLeadTools } from "./tools/leads.js";
|
|
|
9
9
|
import { registerSignalTools } from "./tools/signals.js";
|
|
10
10
|
import { registerIcpTools } from "./tools/icp.js";
|
|
11
11
|
import { registerOutreachTools } from "./tools/outreach.js";
|
|
12
|
+
import { registerSocialListeningTools } from "./tools/social-listening.js";
|
|
13
|
+
import { registerLeadSearchTools } from "./tools/lead-search.js";
|
|
12
14
|
/**
|
|
13
15
|
* Creates and configures the Updately MCP server with all tools registered.
|
|
14
16
|
*/
|
|
@@ -28,5 +30,7 @@ export function createServer() {
|
|
|
28
30
|
registerSignalTools(server);
|
|
29
31
|
registerIcpTools(server);
|
|
30
32
|
registerOutreachTools(server);
|
|
33
|
+
registerSocialListeningTools(server);
|
|
34
|
+
registerLeadSearchTools(server);
|
|
31
35
|
return server;
|
|
32
36
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiPost, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerLeadSearchTools(server) {
|
|
4
|
+
server.tool("search_leads", "Search for LinkedIn leads using a natural-language query. " +
|
|
5
|
+
"Examples: 'DevOps engineers in San Francisco at companies with 100+ employees', " +
|
|
6
|
+
"'VP of Sales at SaaS companies in New York', " +
|
|
7
|
+
"'founders of AI startups in Bangalore'. " +
|
|
8
|
+
"Returns matching LinkedIn profiles with name, headline, company, location, and profile URL.", {
|
|
9
|
+
query: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Natural-language lead search query (e.g. 'DevOps in SF, companies with 100+ employees')"),
|
|
12
|
+
}, async ({ query }) => {
|
|
13
|
+
const res = await apiPost("/chat/action", { message: query });
|
|
14
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
15
|
+
});
|
|
16
|
+
server.tool("confirm_lead_action", "Confirm and execute a suggested action from search_leads. " +
|
|
17
|
+
"After search_leads returns results with a suggestion (e.g. create a signal, " +
|
|
18
|
+
"ICP, or campaign from the search), use this to confirm and execute it.", {
|
|
19
|
+
type: z
|
|
20
|
+
.enum(["leadSearch", "createSignal", "createIcp", "createCampaign"])
|
|
21
|
+
.describe("The action type returned by search_leads"),
|
|
22
|
+
suggestion: z
|
|
23
|
+
.record(z.unknown())
|
|
24
|
+
.describe("The suggestion object returned by search_leads — pass it as-is"),
|
|
25
|
+
}, async ({ type, suggestion }) => {
|
|
26
|
+
const res = await apiPost("/chat/confirm", {
|
|
27
|
+
type,
|
|
28
|
+
suggestion: suggestion,
|
|
29
|
+
});
|
|
30
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
31
|
+
});
|
|
32
|
+
}
|
package/build/tools/signals.js
CHANGED
|
@@ -9,14 +9,23 @@ export function registerSignalTools(server) {
|
|
|
9
9
|
const res = await apiGet(`/signals/${signalId}`);
|
|
10
10
|
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
11
11
|
});
|
|
12
|
-
server.tool("create_signal", "Create a new signal to auto-capture LinkedIn leads. Types
|
|
12
|
+
server.tool("create_signal", "Create a new signal to auto-capture LinkedIn leads. Types:\n" +
|
|
13
|
+
"• KEYWORD_GROUP — monitors social posts matching keywords and captures authors as leads\n" +
|
|
14
|
+
"• LINKEDIN_JOBS — scrapes job postings for hiring signals\n" +
|
|
15
|
+
"• PROFILE_SEARCH — searches LinkedIn profiles by title, industry, company size, location\n" +
|
|
16
|
+
"• NETWORK — discovers leads from your LinkedIn network by title/company/industry keywords", {
|
|
13
17
|
name: z.string().describe("Signal name"),
|
|
14
18
|
type: z
|
|
15
|
-
.enum(["KEYWORD_GROUP", "LINKEDIN_JOBS"])
|
|
19
|
+
.enum(["KEYWORD_GROUP", "LINKEDIN_JOBS", "PROFILE_SEARCH", "NETWORK"])
|
|
16
20
|
.describe("Signal type"),
|
|
17
21
|
listIds: z
|
|
18
22
|
.array(z.string())
|
|
19
23
|
.describe("Lead list IDs to fan captured leads into"),
|
|
24
|
+
enabled: z.boolean().optional().describe("Enable signal (default: true)"),
|
|
25
|
+
icpId: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("ICP profile ID to filter captured leads"),
|
|
20
29
|
// KEYWORD_GROUP fields
|
|
21
30
|
keywordGroupIds: z
|
|
22
31
|
.array(z.string())
|
|
@@ -39,11 +48,48 @@ export function registerSignalTools(server) {
|
|
|
39
48
|
.number()
|
|
40
49
|
.optional()
|
|
41
50
|
.describe("How often to run in hours (min 24, default 24)"),
|
|
42
|
-
|
|
51
|
+
// PROFILE_SEARCH fields
|
|
52
|
+
searchKeywords: z
|
|
43
53
|
.string()
|
|
44
54
|
.optional()
|
|
45
|
-
.describe("
|
|
46
|
-
|
|
55
|
+
.describe("Keywords for profile search (for PROFILE_SEARCH type)"),
|
|
56
|
+
searchTitle: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Job title filter (e.g. 'CTO', 'DevOps Engineer')"),
|
|
60
|
+
searchLocation: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Location for profile search (e.g. 'San Francisco')"),
|
|
64
|
+
searchCompanySizeMin: z
|
|
65
|
+
.number()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Min company size (e.g. 50)"),
|
|
68
|
+
searchCompanySizeMax: z
|
|
69
|
+
.number()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Max company size (e.g. 500)"),
|
|
72
|
+
searchMaxResults: z
|
|
73
|
+
.number()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Max profiles to return (default: 25)"),
|
|
76
|
+
// NETWORK fields
|
|
77
|
+
networkTitleKeywords: z
|
|
78
|
+
.array(z.string())
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Title keywords to match in your network (for NETWORK type, e.g. ['CTO', 'VP Engineering'])"),
|
|
81
|
+
networkCompanyKeywords: z
|
|
82
|
+
.array(z.string())
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Company keywords to match (e.g. ['Google', 'Microsoft'])"),
|
|
85
|
+
networkIndustryKeywords: z
|
|
86
|
+
.array(z.string())
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Industry keywords to match (e.g. ['SaaS', 'Fintech'])"),
|
|
89
|
+
networkMaxResults: z
|
|
90
|
+
.number()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Max network leads to return (default: 25)"),
|
|
47
93
|
}, async (args) => {
|
|
48
94
|
const res = await apiPost("/signals", args);
|
|
49
95
|
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiGet, apiPost, formatResponse } from "../api-client.js";
|
|
3
|
+
export function registerSocialListeningTools(server) {
|
|
4
|
+
server.tool("list_keyword_groups", "List all keyword groups configured for social listening. " +
|
|
5
|
+
"Each group has a name and a list of tracked keywords. " +
|
|
6
|
+
"Use the group names as filters in fetch_posts.", {}, async () => {
|
|
7
|
+
const res = await apiGet("/org/groupkeywords");
|
|
8
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
9
|
+
});
|
|
10
|
+
server.tool("fetch_posts", "Fetch social-media posts matching keyword groups and/or platforms. " +
|
|
11
|
+
"Returns scored, ranked posts with author info, engagement metrics, " +
|
|
12
|
+
"sentiment, priority (URGENT/HIGH/MEDIUM/LOW), and lead-type tags. " +
|
|
13
|
+
"Use list_keyword_groups first to get available group names.", {
|
|
14
|
+
keywordGroupNames: z
|
|
15
|
+
.array(z.string())
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Filter by keyword group names (e.g. ['AI Tools', 'DevOps']). " +
|
|
18
|
+
"Omit to fetch across all groups."),
|
|
19
|
+
platforms: z
|
|
20
|
+
.array(z.enum([
|
|
21
|
+
"REDDIT",
|
|
22
|
+
"TWITTER",
|
|
23
|
+
"LINKEDIN",
|
|
24
|
+
"QUORA",
|
|
25
|
+
"HACKER_NEWS",
|
|
26
|
+
"GITHUB",
|
|
27
|
+
"STACK_OVERFLOW",
|
|
28
|
+
]))
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Filter by platform. Omit to fetch across all platforms."),
|
|
31
|
+
priorities: z
|
|
32
|
+
.array(z.enum(["P(-1)", "P(-2)", "P(-3)", "P(-4)"]))
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Filter by priority level: P(-1)=URGENT, P(-2)=HIGH, P(-3)=MEDIUM, P(-4)=LOW"),
|
|
35
|
+
sentiments: z
|
|
36
|
+
.array(z.enum(["POSITIVE", "NEGATIVE", "NEUTRAL"]))
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Filter by sentiment"),
|
|
39
|
+
sortBy: z
|
|
40
|
+
.enum(["LATEST", "RELEVANCE", "PRIORITY"])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Sort order (default: RELEVANCE)"),
|
|
43
|
+
page: z
|
|
44
|
+
.number()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Page number, 1-indexed (default: 1)"),
|
|
47
|
+
limit: z
|
|
48
|
+
.number()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Results per page (default: 10, max: 50)"),
|
|
51
|
+
}, async ({ keywordGroupNames, platforms, priorities, sentiments, sortBy, page, limit, }) => {
|
|
52
|
+
const effectivePage = page ?? 1;
|
|
53
|
+
const effectiveLimit = Math.min(limit ?? 10, 50);
|
|
54
|
+
const body = {
|
|
55
|
+
sortField: sortBy ?? "RELEVANCE",
|
|
56
|
+
sortOrder: "DESC",
|
|
57
|
+
keywordGroupNameFilters: keywordGroupNames ?? [],
|
|
58
|
+
platformFilters: platforms ?? [],
|
|
59
|
+
priorityFilters: priorities ?? [],
|
|
60
|
+
sentimentFilters: sentiments ?? [],
|
|
61
|
+
leadTypeFilters: [],
|
|
62
|
+
offset: (effectivePage - 1) * effectiveLimit,
|
|
63
|
+
limit: effectiveLimit,
|
|
64
|
+
excludedUnifiedIds: [],
|
|
65
|
+
};
|
|
66
|
+
const res = await apiPost("/org/posts/fetch", body);
|
|
67
|
+
return { content: [{ type: "text", text: formatResponse(res) }] };
|
|
68
|
+
});
|
|
69
|
+
server.tool("save_post", "Save/bookmark a post from the social listening feed for follow-up later.", {
|
|
70
|
+
postId: z
|
|
71
|
+
.string()
|
|
72
|
+
.describe("The unifiedId of the post to save (from fetch_posts results)"),
|
|
73
|
+
}, async ({ postId }) => {
|
|
74
|
+
// The backend expects the full Post object; we pass minimal shape
|
|
75
|
+
const res = await apiPost("/org/posts/save", [
|
|
76
|
+
{ post: { unifiedId: postId } },
|
|
77
|
+
]);
|
|
78
|
+
return { content: [{ type: "text", text: formatResponse(res.data) }] };
|
|
79
|
+
});
|
|
80
|
+
server.tool("get_saved_posts", "List all previously saved/bookmarked posts from social listening.", {
|
|
81
|
+
page: z.number().optional().describe("Page number (default: 1)"),
|
|
82
|
+
}, async ({ page }) => {
|
|
83
|
+
const effectivePage = page ?? 1;
|
|
84
|
+
const body = {
|
|
85
|
+
sortField: "LATEST",
|
|
86
|
+
sortOrder: "DESC",
|
|
87
|
+
keywordGroupNameFilters: [],
|
|
88
|
+
platformFilters: [],
|
|
89
|
+
priorityFilters: [],
|
|
90
|
+
sentimentFilters: [],
|
|
91
|
+
leadTypeFilters: [],
|
|
92
|
+
offset: (effectivePage - 1) * 10,
|
|
93
|
+
limit: 10,
|
|
94
|
+
excludedUnifiedIds: [],
|
|
95
|
+
};
|
|
96
|
+
const res = await apiPost("/org/posts/saved", body);
|
|
97
|
+
return { content: [{ type: "text", text: formatResponse(res) }] };
|
|
98
|
+
});
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@updately/mcp-server",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for Updately LinkedIn Outreach — exposes campaign management, contacts, inbox, signals, ICP and more as AI-callable tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|