outx-mcp-server 1.2.1 → 1.7.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.
Potentially problematic release.
This version of outx-mcp-server might be problematic. Click here for more details.
- package/README.md +20 -22
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/account.d.ts +3 -0
- package/dist/tools/account.js +11 -0
- package/dist/tools/account.js.map +1 -0
- package/dist/tools/engagement.js +4 -4
- package/dist/tools/engagement.js.map +1 -1
- package/dist/tools/linkedin.js +24 -8
- package/dist/tools/linkedin.js.map +1 -1
- package/dist/tools/posts.js +2 -2
- package/dist/tools/posts.js.map +1 -1
- package/dist/tools/watchlists.js +260 -69
- package/dist/tools/watchlists.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/tools/account.ts +21 -0
- package/src/tools/engagement.ts +6 -6
- package/src/tools/linkedin.ts +39 -14
- package/src/tools/posts.ts +2 -2
- package/src/tools/watchlists.ts +306 -77
package/src/tools/linkedin.ts
CHANGED
|
@@ -25,8 +25,8 @@ export function registerLinkedInTools(
|
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
server.tool(
|
|
28
|
-
"
|
|
29
|
-
"Fetch recent posts from LinkedIn profiles by URN. Use fetch_linkedin_profile first to get the profile URN. Requires the OutX Chrome extension to be active. This tool handles async polling automatically.",
|
|
28
|
+
"fetch_linkedin_profile_posts",
|
|
29
|
+
"Fetch recent posts from one or more LinkedIn profiles by URN. Use fetch_linkedin_profile first to get the profile URN. Requires the OutX Chrome extension to be active. This tool handles async polling automatically.",
|
|
30
30
|
{
|
|
31
31
|
profile_urns: z
|
|
32
32
|
.array(z.string())
|
|
@@ -45,8 +45,8 @@ export function registerLinkedInTools(
|
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
server.tool(
|
|
48
|
-
"
|
|
49
|
-
"Like a LinkedIn post by its activity URN.
|
|
48
|
+
"like_linkedin_post",
|
|
49
|
+
"Like a LinkedIn post by its activity URN (direct LinkedIn action). Use this when you have a `social_urn` and don't need a watchlist. For liking a post that came from one of your watchlists (using its OutX `post_id`), use `like_watchlist_post` instead. Requires the OutX Chrome extension to be active. Polls automatically.",
|
|
50
50
|
{
|
|
51
51
|
social_urn: z
|
|
52
52
|
.string()
|
|
@@ -63,8 +63,8 @@ export function registerLinkedInTools(
|
|
|
63
63
|
);
|
|
64
64
|
|
|
65
65
|
server.tool(
|
|
66
|
-
"
|
|
67
|
-
"Comment on a LinkedIn post by its activity URN.
|
|
66
|
+
"comment_on_linkedin_post",
|
|
67
|
+
"Comment on a LinkedIn post by its activity URN (direct LinkedIn action). Use this when you have a `social_urn` and don't need a watchlist. For commenting on a post that came from one of your watchlists (using its OutX `post_id`), use `comment_on_watchlist_post` instead. Requires the OutX Chrome extension to be active. Polls automatically.",
|
|
68
68
|
{
|
|
69
69
|
social_urn: z
|
|
70
70
|
.string()
|
|
@@ -119,8 +119,8 @@ export function registerLinkedInTools(
|
|
|
119
119
|
);
|
|
120
120
|
|
|
121
121
|
server.tool(
|
|
122
|
-
"
|
|
123
|
-
"Send a LinkedIn direct message to a 1st-degree connection. Requires the recipient's person URN (get it from fetch_linkedin_profile or
|
|
122
|
+
"send_linkedin_message",
|
|
123
|
+
"Send a LinkedIn direct message to a 1st-degree connection. Requires the recipient's person URN (get it from fetch_linkedin_profile or search_linkedin_profiles). Message is sent from your team's oldest admin member's LinkedIn account. Requires the OutX Chrome extension to be active. Polls automatically.",
|
|
124
124
|
{
|
|
125
125
|
recipient_urn: z
|
|
126
126
|
.string()
|
|
@@ -141,8 +141,33 @@ export function registerLinkedInTools(
|
|
|
141
141
|
);
|
|
142
142
|
|
|
143
143
|
server.tool(
|
|
144
|
-
"
|
|
145
|
-
"
|
|
144
|
+
"send_linkedin_connection_request",
|
|
145
|
+
"Send a LinkedIn connection request to a profile, with an optional note. Requires the person URN (get it from fetch_linkedin_profile or search_linkedin_profiles). Sent from your team's oldest admin member's LinkedIn account. Requires the OutX Chrome extension to be active. Polls automatically.",
|
|
146
|
+
{
|
|
147
|
+
profile_urn: z
|
|
148
|
+
.string()
|
|
149
|
+
.describe("LinkedIn person URN to connect with (e.g. 'urn:li:person:ACoAABCDEFG')"),
|
|
150
|
+
note: z
|
|
151
|
+
.string()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("Optional connection note (max 300 characters)."),
|
|
154
|
+
},
|
|
155
|
+
async (params) => {
|
|
156
|
+
const body: Record<string, unknown> = { profile_urn: params.profile_urn };
|
|
157
|
+
if (params.note) body.note = params.note;
|
|
158
|
+
const response = (await client.post(
|
|
159
|
+
"/linkedin-agent/send-connection-request",
|
|
160
|
+
body
|
|
161
|
+
)) as { api_agent_task_id: string };
|
|
162
|
+
|
|
163
|
+
const result = await client.pollTask(response.api_agent_task_id);
|
|
164
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
server.tool(
|
|
169
|
+
"search_linkedin_profiles",
|
|
170
|
+
"Search LinkedIn profiles by keywords, title, company, location, industry, and more. Returns matching profiles with name, headline, location, profile slug, and connection degree. At least one filter is required. Requires the OutX Chrome extension to be active. Polls automatically.",
|
|
146
171
|
{
|
|
147
172
|
keywords: z.string().optional().describe("General keyword search across name, headline, and profile text"),
|
|
148
173
|
title: z.string().optional().describe("Filter by job title (e.g. 'VP of Engineering')"),
|
|
@@ -182,8 +207,8 @@ export function registerLinkedInTools(
|
|
|
182
207
|
);
|
|
183
208
|
|
|
184
209
|
server.tool(
|
|
185
|
-
"
|
|
186
|
-
"Fetch your 1st-degree LinkedIn connections. Optionally filter by keyword, sort by recently added, and paginate. Connections are from your team's oldest admin member's LinkedIn account. Requires the OutX Chrome extension to be active.
|
|
210
|
+
"fetch_linkedin_connections",
|
|
211
|
+
"Fetch your 1st-degree LinkedIn connections. Optionally filter by keyword, sort by recently added, and paginate. Connections are from your team's oldest admin member's LinkedIn account. Requires the OutX Chrome extension to be active. Polls automatically.",
|
|
187
212
|
{
|
|
188
213
|
keyword: z.string().optional().describe("Filter connections by name (e.g. 'John')"),
|
|
189
214
|
sort_type: z.string().optional().describe("Sort order. Pass 'RECENTLY_ADDED' for newest first"),
|
|
@@ -208,9 +233,9 @@ export function registerLinkedInTools(
|
|
|
208
233
|
|
|
209
234
|
server.tool(
|
|
210
235
|
"get_task_status",
|
|
211
|
-
"
|
|
236
|
+
"Recovery tool. Every other LinkedIn Data API tool polls automatically for ~150 seconds and returns the result directly, so you almost never need this. Use it ONLY when another tool returns a 'did not complete within 150s' error: pass the `api_agent_task_id` from that error message to check whether the task has since finished. Calling the original tool again would create a duplicate task and consume another LinkedIn-side daily quota slot. Status values: pending, processing, completed, failed.",
|
|
212
237
|
{
|
|
213
|
-
task_id: z.string().describe("The api_agent_task_id
|
|
238
|
+
task_id: z.string().describe("The `api_agent_task_id` from the timeout error message of another LinkedIn Data tool."),
|
|
214
239
|
},
|
|
215
240
|
async (params) => {
|
|
216
241
|
const result = await client.get("/linkedin-agent/get-task-status", {
|
package/src/tools/posts.ts
CHANGED
|
@@ -13,9 +13,9 @@ export function registerPostTools(
|
|
|
13
13
|
watchlist_id: z.string().optional().describe("Filter by watchlist ID (omit for all)"),
|
|
14
14
|
search_term: z.string().optional().describe("Search within post content"),
|
|
15
15
|
sort_by: z
|
|
16
|
-
.enum(["
|
|
16
|
+
.enum(["recent", "popular_first", "engagement", "relevance_first"])
|
|
17
17
|
.optional()
|
|
18
|
-
.describe("Sort order (default:
|
|
18
|
+
.describe("Sort order (default: recent)"),
|
|
19
19
|
page: z.string().optional().describe("Page number (default: 1)"),
|
|
20
20
|
start_date: z.string().optional().describe("Filter posts after this date (ISO 8601)"),
|
|
21
21
|
end_date: z.string().optional().describe("Filter posts before this date (ISO 8601)"),
|
package/src/tools/watchlists.ts
CHANGED
|
@@ -6,6 +6,41 @@ const fetchFreqEnum = z
|
|
|
6
6
|
.enum(["1", "3", "6", "12", "24", "48", "72"])
|
|
7
7
|
.describe("How often to scan for new posts (hours)");
|
|
8
8
|
|
|
9
|
+
// A label (intent category) is either a bare name string, or an object with an
|
|
10
|
+
// optional `default` flag that controls whether the label shows in the feed by
|
|
11
|
+
// default. At most 2 labels may be default-true; the API caps any extras to false.
|
|
12
|
+
// `key` is accepted as an alias for `name` so a label object returned by a GET
|
|
13
|
+
// (which uses `key`) can be passed straight back on update.
|
|
14
|
+
const labelSchema = z.union([
|
|
15
|
+
z.string(),
|
|
16
|
+
z.object({
|
|
17
|
+
name: z.string().optional().describe("Label name (use for new labels)"),
|
|
18
|
+
key: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Alias for `name`; the value GET returns, so a label from a GET can be sent back unchanged"),
|
|
22
|
+
description: z.string().optional().describe("What this label represents"),
|
|
23
|
+
default: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(
|
|
27
|
+
"Show this label in the feed by default. At most 2 labels may be true; extras are capped to false."
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
type LabelParam =
|
|
33
|
+
| string
|
|
34
|
+
| { name?: string; key?: string; description?: string; default?: boolean };
|
|
35
|
+
|
|
36
|
+
// Normalize to the object array the API expects. A bare string becomes { name },
|
|
37
|
+
// so callers that pass plain label names keep working (and now actually persist).
|
|
38
|
+
function toApiLabels(
|
|
39
|
+
labels: LabelParam[]
|
|
40
|
+
): Array<{ name?: string; key?: string; description?: string; default?: boolean }> {
|
|
41
|
+
return labels.map((l) => (typeof l === "string" ? { name: l } : l));
|
|
42
|
+
}
|
|
43
|
+
|
|
9
44
|
export function registerWatchlistTools(
|
|
10
45
|
server: McpServer,
|
|
11
46
|
client: OutxClient
|
|
@@ -14,37 +49,72 @@ export function registerWatchlistTools(
|
|
|
14
49
|
|
|
15
50
|
server.tool(
|
|
16
51
|
"create_keyword_watchlist",
|
|
17
|
-
"Create a watchlist that monitors LinkedIn posts matching keywords.
|
|
52
|
+
"Create a watchlist that monitors LinkedIn posts matching keywords. Two modes: provide `keywords` to track exact terms, OR provide `prompt` to have OutX generate keywords and intent labels from a plain-English description. Pass one or the other, not both.",
|
|
18
53
|
{
|
|
19
54
|
keywords: z
|
|
20
55
|
.array(z.string())
|
|
21
|
-
.
|
|
22
|
-
.describe(
|
|
56
|
+
.optional()
|
|
57
|
+
.describe(
|
|
58
|
+
"Direct mode. Keywords to track (e.g. ['AI startup funding', 'series A']). Mutually exclusive with `prompt`."
|
|
59
|
+
),
|
|
23
60
|
required_keywords: z
|
|
24
61
|
.array(z.string())
|
|
25
62
|
.optional()
|
|
26
|
-
.describe("Posts MUST contain at least one of these words"),
|
|
63
|
+
.describe("Direct mode only. Applied to every keyword. Posts MUST contain at least one of these words."),
|
|
27
64
|
exclude_keywords: z
|
|
28
65
|
.array(z.string())
|
|
29
66
|
.optional()
|
|
30
|
-
.describe("
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
67
|
+
.describe("Direct mode only. Applied to every keyword. Posts containing any of these are excluded."),
|
|
68
|
+
prompt: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe(
|
|
72
|
+
"AI mode. Plain-English description of what to track (e.g. 'People looking for remote software engineering jobs'). OutX generates keywords and intent labels in the background. Mutually exclusive with `keywords`."
|
|
73
|
+
),
|
|
74
|
+
name: z.string().optional().describe("Watchlist name. Auto-generated if omitted."),
|
|
75
|
+
description: z.string().optional().describe("Direct mode only. Optional description for the watchlist."),
|
|
76
|
+
objective: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe(
|
|
80
|
+
"Direct mode only. One-sentence objective (who/what you want to find and why). Drives the relevance-scoring vector so direct-mode lists rank like prompt-built ones. Omit and the list ranks only by keyword match."
|
|
81
|
+
),
|
|
82
|
+
labels: z
|
|
83
|
+
.array(labelSchema)
|
|
84
|
+
.optional()
|
|
85
|
+
.describe(
|
|
86
|
+
"Direct mode only. Intent labels for categorization. Each is a name string, or an object {name, description, default} where `default` controls feed visibility (max 2 default-true)."
|
|
87
|
+
),
|
|
34
88
|
fetchFreqInHours: fetchFreqEnum.optional(),
|
|
35
89
|
},
|
|
36
90
|
async (params) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
91
|
+
const hasKeywords = Array.isArray(params.keywords) && params.keywords.length > 0;
|
|
92
|
+
const hasPrompt = typeof params.prompt === "string" && params.prompt.trim().length > 0;
|
|
93
|
+
|
|
94
|
+
if (hasKeywords && hasPrompt) {
|
|
95
|
+
throw new Error("Provide either `keywords` or `prompt`, not both.");
|
|
96
|
+
}
|
|
97
|
+
if (!hasKeywords && !hasPrompt) {
|
|
98
|
+
throw new Error("Provide either `keywords` (array) or `prompt` (string).");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const body: Record<string, unknown> = {};
|
|
102
|
+
|
|
103
|
+
if (hasPrompt) {
|
|
104
|
+
body.prompt = params.prompt!.trim();
|
|
105
|
+
} else {
|
|
106
|
+
body.keywords = params.keywords!.map((kw) => {
|
|
39
107
|
const obj: Record<string, unknown> = { keyword: kw };
|
|
40
108
|
if (params.required_keywords) obj.required_keywords = params.required_keywords;
|
|
41
109
|
if (params.exclude_keywords) obj.exclude_keywords = params.exclude_keywords;
|
|
42
110
|
return obj;
|
|
43
|
-
})
|
|
44
|
-
|
|
111
|
+
});
|
|
112
|
+
if (params.description) body.description = params.description;
|
|
113
|
+
if (params.objective) body.objective = params.objective;
|
|
114
|
+
if (params.labels) body.labels = toApiLabels(params.labels);
|
|
115
|
+
}
|
|
116
|
+
|
|
45
117
|
if (params.name) body.name = params.name;
|
|
46
|
-
if (params.description) body.description = params.description;
|
|
47
|
-
if (params.labels) body.labels = params.labels;
|
|
48
118
|
if (params.fetchFreqInHours)
|
|
49
119
|
body.fetchFreqInHours = parseInt(params.fetchFreqInHours);
|
|
50
120
|
|
|
@@ -65,7 +135,7 @@ export function registerWatchlistTools(
|
|
|
65
135
|
|
|
66
136
|
server.tool(
|
|
67
137
|
"update_keyword_watchlist",
|
|
68
|
-
"Update a keyword watchlist.
|
|
138
|
+
"Update a keyword watchlist. Patch any fields (name, fetchFreqInHours, disable, slack_webhook_url, keywords, labels), OR pass `prompt` to have OutX regenerate keywords and labels from a new description. Patch and prompt modes are mutually exclusive.",
|
|
69
139
|
{
|
|
70
140
|
id: z.string().describe("Watchlist ID"),
|
|
71
141
|
name: z.string().optional(),
|
|
@@ -83,38 +153,66 @@ export function registerWatchlistTools(
|
|
|
83
153
|
.array(z.string())
|
|
84
154
|
.optional()
|
|
85
155
|
.describe(
|
|
86
|
-
"Replace tracked keywords
|
|
156
|
+
"Patch mode. Replace tracked keywords; all existing keywords are removed and these are inserted. Mutually exclusive with `prompt`."
|
|
87
157
|
),
|
|
88
158
|
required_keywords: z
|
|
89
159
|
.array(z.string())
|
|
90
160
|
.optional()
|
|
91
|
-
.describe("Applied to every keyword above. Posts MUST contain at least one."),
|
|
161
|
+
.describe("Patch mode only. Applied to every keyword above. Posts MUST contain at least one."),
|
|
92
162
|
exclude_keywords: z
|
|
93
163
|
.array(z.string())
|
|
94
164
|
.optional()
|
|
95
|
-
.describe("Applied to every keyword above. Posts containing any are excluded."),
|
|
165
|
+
.describe("Patch mode only. Applied to every keyword above. Posts containing any are excluded."),
|
|
96
166
|
labels: z
|
|
97
|
-
.array(
|
|
167
|
+
.array(labelSchema)
|
|
98
168
|
.optional()
|
|
99
|
-
.describe(
|
|
169
|
+
.describe(
|
|
170
|
+
"Patch mode only. Replaces the label set. Each is a name string, or an object {name, description, default}. GET the watchlist first to see current labels and their `default` flags so you can preserve them."
|
|
171
|
+
),
|
|
172
|
+
prompt: z
|
|
173
|
+
.string()
|
|
174
|
+
.optional()
|
|
175
|
+
.describe(
|
|
176
|
+
"Regenerate mode. New plain-English prompt. OutX wipes the existing keywords and labels and regenerates them in the background. Mutually exclusive with patch fields above (other than `id`)."
|
|
177
|
+
),
|
|
100
178
|
},
|
|
101
179
|
async (params) => {
|
|
180
|
+
const hasPrompt = typeof params.prompt === "string" && params.prompt.trim().length > 0;
|
|
181
|
+
const hasPatch =
|
|
182
|
+
params.name !== undefined ||
|
|
183
|
+
params.fetchFreqInHours !== undefined ||
|
|
184
|
+
params.disable !== undefined ||
|
|
185
|
+
params.slack_webhook_url !== undefined ||
|
|
186
|
+
params.keywords !== undefined ||
|
|
187
|
+
params.labels !== undefined;
|
|
188
|
+
|
|
189
|
+
if (hasPrompt && hasPatch) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Provide either patch fields (name, fetchFreqInHours, disable, slack_webhook_url, keywords, labels) or `prompt` for regeneration, not both."
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
102
195
|
const body: Record<string, unknown> = { id: params.id };
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
body.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
body.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
196
|
+
|
|
197
|
+
if (hasPrompt) {
|
|
198
|
+
body.prompt = params.prompt!.trim();
|
|
199
|
+
} else {
|
|
200
|
+
if (params.name) body.name = params.name;
|
|
201
|
+
if (params.fetchFreqInHours)
|
|
202
|
+
body.fetchFreqInHours = parseInt(params.fetchFreqInHours);
|
|
203
|
+
if (params.disable !== undefined) body.disable = params.disable;
|
|
204
|
+
if (params.slack_webhook_url !== undefined)
|
|
205
|
+
body.slack_webhook_url = params.slack_webhook_url;
|
|
206
|
+
if (params.keywords) {
|
|
207
|
+
body.keywords = params.keywords.map((kw) => {
|
|
208
|
+
const obj: Record<string, unknown> = { keyword: kw };
|
|
209
|
+
if (params.required_keywords) obj.required_keywords = params.required_keywords;
|
|
210
|
+
if (params.exclude_keywords) obj.exclude_keywords = params.exclude_keywords;
|
|
211
|
+
return obj;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (params.labels) body.labels = toApiLabels(params.labels);
|
|
116
215
|
}
|
|
117
|
-
if (params.labels) body.labels = params.labels;
|
|
118
216
|
|
|
119
217
|
const result = await client.put("/api-keyword-watchlist", body);
|
|
120
218
|
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
@@ -135,49 +233,6 @@ export function registerWatchlistTools(
|
|
|
135
233
|
}
|
|
136
234
|
);
|
|
137
235
|
|
|
138
|
-
server.tool(
|
|
139
|
-
"create_keyword_watchlist_from_prompt",
|
|
140
|
-
"Create a keyword watchlist from a natural-language prompt. OutX generates optimized keywords and intent labels automatically using AI. The watchlist is created immediately; keyword generation runs in the background. Hits the same endpoint as create_keyword_watchlist with a different body shape (prompt instead of keywords).",
|
|
141
|
-
{
|
|
142
|
-
prompt: z
|
|
143
|
-
.string()
|
|
144
|
-
.describe(
|
|
145
|
-
"Plain English description of what to track (e.g. 'People looking for remote software engineering jobs'). Can include URLs for better keyword quality."
|
|
146
|
-
),
|
|
147
|
-
name: z.string().optional().describe("Watchlist name (auto-generated if omitted)"),
|
|
148
|
-
fetchFreqInHours: fetchFreqEnum.optional(),
|
|
149
|
-
},
|
|
150
|
-
async (params) => {
|
|
151
|
-
const body: Record<string, unknown> = { prompt: params.prompt };
|
|
152
|
-
if (params.name) body.name = params.name;
|
|
153
|
-
if (params.fetchFreqInHours)
|
|
154
|
-
body.fetchFreqInHours = parseInt(params.fetchFreqInHours);
|
|
155
|
-
|
|
156
|
-
const result = await client.post("/api-keyword-watchlist", body);
|
|
157
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
158
|
-
}
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
server.tool(
|
|
162
|
-
"regenerate_keyword_watchlist_from_prompt",
|
|
163
|
-
"Replace the prompt on an existing keyword watchlist. OutX wipes the current keywords and labels and regenerates them from the new prompt. Regeneration runs in the background. Hits the same endpoint as update_keyword_watchlist with a prompt body.",
|
|
164
|
-
{
|
|
165
|
-
id: z.string().describe("Watchlist ID to update"),
|
|
166
|
-
prompt: z
|
|
167
|
-
.string()
|
|
168
|
-
.describe("New natural-language prompt. OutX will regenerate keywords and labels from this."),
|
|
169
|
-
},
|
|
170
|
-
async (params) => {
|
|
171
|
-
const body: Record<string, unknown> = {
|
|
172
|
-
id: params.id,
|
|
173
|
-
prompt: params.prompt,
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const result = await client.put("/api-keyword-watchlist", body);
|
|
177
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
178
|
-
}
|
|
179
|
-
);
|
|
180
|
-
|
|
181
236
|
// ── People Watchlists ───────────────────────────────────
|
|
182
237
|
|
|
183
238
|
server.tool(
|
|
@@ -343,4 +398,178 @@ export function registerWatchlistTools(
|
|
|
343
398
|
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
344
399
|
}
|
|
345
400
|
);
|
|
401
|
+
|
|
402
|
+
// ── Reddit Watchlists ───────────────────────────────────
|
|
403
|
+
|
|
404
|
+
server.tool(
|
|
405
|
+
"create_reddit_watchlist",
|
|
406
|
+
"Create a watchlist that monitors Reddit posts matching keywords. Two modes: provide `keywords` to track exact terms, OR provide `prompt` to have OutX generate keywords and intent labels from a plain-English description. Pass one or the other, not both.",
|
|
407
|
+
{
|
|
408
|
+
keywords: z
|
|
409
|
+
.array(z.string())
|
|
410
|
+
.optional()
|
|
411
|
+
.describe("Direct mode. Keywords to track on Reddit. Mutually exclusive with `prompt`."),
|
|
412
|
+
required_keywords: z
|
|
413
|
+
.array(z.string())
|
|
414
|
+
.optional()
|
|
415
|
+
.describe("Direct mode only. Applied to every keyword. Posts MUST contain at least one of these words."),
|
|
416
|
+
exclude_keywords: z
|
|
417
|
+
.array(z.string())
|
|
418
|
+
.optional()
|
|
419
|
+
.describe("Direct mode only. Applied to every keyword. Posts containing any of these are excluded."),
|
|
420
|
+
prompt: z
|
|
421
|
+
.string()
|
|
422
|
+
.optional()
|
|
423
|
+
.describe("AI mode. Plain-English description of what to track on Reddit. Mutually exclusive with `keywords`."),
|
|
424
|
+
name: z.string().optional().describe("Watchlist name. Auto-generated if omitted."),
|
|
425
|
+
description: z.string().optional().describe("Direct mode only. Optional description for the watchlist."),
|
|
426
|
+
objective: z
|
|
427
|
+
.string()
|
|
428
|
+
.optional()
|
|
429
|
+
.describe("Direct mode only. One-sentence objective; drives the relevance-scoring vector so direct-mode lists rank like prompt-built ones."),
|
|
430
|
+
labels: z
|
|
431
|
+
.array(labelSchema)
|
|
432
|
+
.optional()
|
|
433
|
+
.describe("Direct mode only. Intent labels. Each is a name string, or an object {name, description, default} (max 2 default-true)."),
|
|
434
|
+
fetchFreqInHours: fetchFreqEnum.optional().describe("Scan frequency in hours (Reddit default 24)."),
|
|
435
|
+
},
|
|
436
|
+
async (params) => {
|
|
437
|
+
const hasKeywords = Array.isArray(params.keywords) && params.keywords.length > 0;
|
|
438
|
+
const hasPrompt = typeof params.prompt === "string" && params.prompt.trim().length > 0;
|
|
439
|
+
|
|
440
|
+
if (hasKeywords && hasPrompt) {
|
|
441
|
+
throw new Error("Provide either `keywords` or `prompt`, not both.");
|
|
442
|
+
}
|
|
443
|
+
if (!hasKeywords && !hasPrompt) {
|
|
444
|
+
throw new Error("Provide either `keywords` (array) or `prompt` (string).");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const body: Record<string, unknown> = {};
|
|
448
|
+
|
|
449
|
+
if (hasPrompt) {
|
|
450
|
+
body.prompt = params.prompt!.trim();
|
|
451
|
+
} else {
|
|
452
|
+
body.keywords = params.keywords!.map((kw) => {
|
|
453
|
+
const obj: Record<string, unknown> = { keyword: kw };
|
|
454
|
+
if (params.required_keywords) obj.required_keywords = params.required_keywords;
|
|
455
|
+
if (params.exclude_keywords) obj.exclude_keywords = params.exclude_keywords;
|
|
456
|
+
return obj;
|
|
457
|
+
});
|
|
458
|
+
if (params.description) body.description = params.description;
|
|
459
|
+
if (params.objective) body.objective = params.objective;
|
|
460
|
+
if (params.labels) body.labels = toApiLabels(params.labels);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (params.name) body.name = params.name;
|
|
464
|
+
if (params.fetchFreqInHours)
|
|
465
|
+
body.fetchFreqInHours = parseInt(params.fetchFreqInHours);
|
|
466
|
+
|
|
467
|
+
const result = await client.post("/api-reddit-watchlist", body);
|
|
468
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
server.tool(
|
|
473
|
+
"list_reddit_watchlists",
|
|
474
|
+
"List all reddit watchlists for your account.",
|
|
475
|
+
{},
|
|
476
|
+
async () => {
|
|
477
|
+
const result = await client.get("/api-reddit-watchlist");
|
|
478
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
server.tool(
|
|
483
|
+
"update_reddit_watchlist",
|
|
484
|
+
"Update a reddit watchlist. Patch any fields (name, fetchFreqInHours, disable, slack_webhook_url, keywords, labels), OR pass `prompt` to regenerate keywords and labels. Patch and prompt modes are mutually exclusive.",
|
|
485
|
+
{
|
|
486
|
+
id: z.string().describe("Watchlist ID"),
|
|
487
|
+
name: z.string().optional(),
|
|
488
|
+
fetchFreqInHours: fetchFreqEnum.optional(),
|
|
489
|
+
disable: z
|
|
490
|
+
.boolean()
|
|
491
|
+
.optional()
|
|
492
|
+
.describe("Set to true to pause tracking, false to resume"),
|
|
493
|
+
slack_webhook_url: z
|
|
494
|
+
.string()
|
|
495
|
+
.nullable()
|
|
496
|
+
.optional()
|
|
497
|
+
.describe("Slack incoming webhook URL for alerts. Pass null to clear."),
|
|
498
|
+
keywords: z
|
|
499
|
+
.array(z.string())
|
|
500
|
+
.optional()
|
|
501
|
+
.describe("Patch mode. Replace tracked keywords. Mutually exclusive with `prompt`."),
|
|
502
|
+
required_keywords: z
|
|
503
|
+
.array(z.string())
|
|
504
|
+
.optional()
|
|
505
|
+
.describe("Patch mode only. Applied to every keyword above."),
|
|
506
|
+
exclude_keywords: z
|
|
507
|
+
.array(z.string())
|
|
508
|
+
.optional()
|
|
509
|
+
.describe("Patch mode only. Applied to every keyword above."),
|
|
510
|
+
labels: z
|
|
511
|
+
.array(labelSchema)
|
|
512
|
+
.optional()
|
|
513
|
+
.describe("Patch mode only. Replaces the label set. GET the watchlist first to preserve current `default` flags."),
|
|
514
|
+
prompt: z
|
|
515
|
+
.string()
|
|
516
|
+
.optional()
|
|
517
|
+
.describe("Regenerate mode. New plain-English prompt. Mutually exclusive with the patch fields above."),
|
|
518
|
+
},
|
|
519
|
+
async (params) => {
|
|
520
|
+
const hasPrompt = typeof params.prompt === "string" && params.prompt.trim().length > 0;
|
|
521
|
+
const hasPatch =
|
|
522
|
+
params.name !== undefined ||
|
|
523
|
+
params.fetchFreqInHours !== undefined ||
|
|
524
|
+
params.disable !== undefined ||
|
|
525
|
+
params.slack_webhook_url !== undefined ||
|
|
526
|
+
params.keywords !== undefined ||
|
|
527
|
+
params.labels !== undefined;
|
|
528
|
+
|
|
529
|
+
if (hasPrompt && hasPatch) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
"Provide either patch fields (name, fetchFreqInHours, disable, slack_webhook_url, keywords, labels) or `prompt` for regeneration, not both."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const body: Record<string, unknown> = { id: params.id };
|
|
536
|
+
|
|
537
|
+
if (hasPrompt) {
|
|
538
|
+
body.prompt = params.prompt!.trim();
|
|
539
|
+
} else {
|
|
540
|
+
if (params.name) body.name = params.name;
|
|
541
|
+
if (params.fetchFreqInHours)
|
|
542
|
+
body.fetchFreqInHours = parseInt(params.fetchFreqInHours);
|
|
543
|
+
if (params.disable !== undefined) body.disable = params.disable;
|
|
544
|
+
if (params.slack_webhook_url !== undefined)
|
|
545
|
+
body.slack_webhook_url = params.slack_webhook_url;
|
|
546
|
+
if (params.keywords) {
|
|
547
|
+
body.keywords = params.keywords.map((kw) => {
|
|
548
|
+
const obj: Record<string, unknown> = { keyword: kw };
|
|
549
|
+
if (params.required_keywords) obj.required_keywords = params.required_keywords;
|
|
550
|
+
if (params.exclude_keywords) obj.exclude_keywords = params.exclude_keywords;
|
|
551
|
+
return obj;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (params.labels) body.labels = toApiLabels(params.labels);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const result = await client.put("/api-reddit-watchlist", body);
|
|
558
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
server.tool(
|
|
563
|
+
"delete_reddit_watchlist",
|
|
564
|
+
"Delete a reddit watchlist and all its tracked data.",
|
|
565
|
+
{
|
|
566
|
+
id: z.string().describe("Watchlist ID to delete"),
|
|
567
|
+
},
|
|
568
|
+
async (params) => {
|
|
569
|
+
const result = await client.delete("/api-reddit-watchlist", {
|
|
570
|
+
id: params.id,
|
|
571
|
+
});
|
|
572
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
573
|
+
}
|
|
574
|
+
);
|
|
346
575
|
}
|