webdata-tools-mcp 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +104 -2
- package/package.json +46 -4
- package/src/apify.js +103 -0
- package/src/index.js +6 -0
- package/src/server.js +73 -0
- package/src/tools.js +305 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tacps126
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# webdata-tools-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that gives AI agents ten web-data tools: job
|
|
4
|
+
postings, hiring signals, Google News, Trustpilot and app-store reviews, Reddit mentions, website
|
|
5
|
+
contacts, German property listings and Shopify products.
|
|
6
|
+
|
|
7
|
+
Each tool runs a ready-made [Apify](https://apify.com/tacps126) Actor and returns clean JSON. You
|
|
8
|
+
pay Apify per result (about $1–3 per 1,000 results); there is no subscription and nothing is sent
|
|
9
|
+
anywhere except Apify.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
| Tool | What it returns | Cost (approx.) | Actor |
|
|
14
|
+
|------|-----------------|----------------|-------|
|
|
15
|
+
| `search_google_news` | News articles with the publisher's real URL, date, related coverage | $2 / 1,000 articles | [google-news](https://apify.com/tacps126/google-news) |
|
|
16
|
+
| `find_hiring_companies` | Which companies are hiring for given roles (buying-intent signal) | 1¢ per company with a signal | [hiring-signals](https://apify.com/tacps126/hiring-signals) |
|
|
17
|
+
| `get_company_jobs` | All open jobs from career pages (Greenhouse, Lever, Ashby, Workday, …) | $1 / 1,000 jobs | [jobs-harvester](https://apify.com/tacps126/jobs-harvester) |
|
|
18
|
+
| `search_indeed_jobs` | Indeed listings in 9 countries | $3 / 1,000 jobs | [indeed-jobs](https://apify.com/tacps126/indeed-jobs) |
|
|
19
|
+
| `get_trustpilot_reviews` | Reviews, TrustScore and company profile | $0.50 / 1,000 reviews | [trustpilot-reviews](https://apify.com/tacps126/trustpilot-reviews) |
|
|
20
|
+
| `get_app_reviews` | App Store and Google Play reviews | $0.20 / 1,000 reviews | [app-reviews](https://apify.com/tacps126/app-reviews) |
|
|
21
|
+
| `search_reddit` | Reddit posts and comments by keyword or subreddit | $1 / 1,000 items | [reddit-monitor](https://apify.com/tacps126/reddit-monitor) |
|
|
22
|
+
| `find_website_contacts` | Emails, phones, socials from websites | $2 / 1,000 sites with contacts | [contact-details](https://apify.com/tacps126/contact-details) |
|
|
23
|
+
| `search_german_property_listings` | Rent/buy listings in Germany (ImmoScout24) | $1 / 1,000 listings | [immobilienscout24-scraper](https://apify.com/tacps126/immobilienscout24-scraper) |
|
|
24
|
+
| `get_shopify_products` | Prices, discounts, stock, variants from Shopify stores | $1 / 1,000 products | [shopify-products](https://apify.com/tacps126/shopify-products) |
|
|
25
|
+
|
|
26
|
+
Every tool also takes an optional `extra_input` object for any other field the Actor documents.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
1. Create a free [Apify account](https://apify.com) and copy your API token from
|
|
31
|
+
**Console → Settings → API & Integrations**.
|
|
32
|
+
2. Add the server to your client. The token goes in `APIFY_TOKEN`.
|
|
33
|
+
|
|
34
|
+
**Claude Desktop / Cursor / Windsurf** (`mcpServers` config):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"webdata": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "webdata-tools-mcp"],
|
|
42
|
+
"env": { "APIFY_TOKEN": "<your Apify token>" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Claude Code:**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
claude mcp add webdata -e APIFY_TOKEN=<your Apify token> -- npx -y webdata-tools-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires Node.js 18 or newer.
|
|
55
|
+
|
|
56
|
+
## Try it
|
|
57
|
+
|
|
58
|
+
- "Which of these companies are hiring data engineers right now? https://boards.greenhouse.io/stripe, https://jobs.lever.co/spotify"
|
|
59
|
+
- "Show the 1 and 2 star Trustpilot reviews of example.com from the last 30 days and group the complaints."
|
|
60
|
+
- "Find news from the last 7 days about open-source LLM licensing, with the real publisher links."
|
|
61
|
+
- "Get contact emails for these 20 agency websites." (use only for lawful business outreach)
|
|
62
|
+
|
|
63
|
+
## Cost safety
|
|
64
|
+
|
|
65
|
+
- Every call is capped at **$0.50** of Apify spend (`WEBDATA_MAX_CHARGE_USD` changes it). Apify stops
|
|
66
|
+
the run when the cap is reached.
|
|
67
|
+
- `max_items` defaults to small numbers (20–100) and has a hard maximum per tool.
|
|
68
|
+
- Long text fields are shortened to 600 characters, and one result is limited to about 60,000
|
|
69
|
+
characters, so a call cannot flood the agent's context.
|
|
70
|
+
|
|
71
|
+
## No install: Apify's hosted MCP server
|
|
72
|
+
|
|
73
|
+
You can also use Apify's own server with these Actors selected, with nothing to install:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"apify": { "url": "https://mcp.apify.com?tools=tacps126/google-news,tacps126/hiring-signals,tacps126/trustpilot-reviews" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It signs you in with OAuth. The tools take the Actors' raw input fields instead of the simplified
|
|
84
|
+
arguments above.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install
|
|
90
|
+
npm test # offline: mapping, errors, size limits, MCP handshake
|
|
91
|
+
node scripts/sync-actor-inputs.mjs # refresh test/fixtures from the published Actors
|
|
92
|
+
APIFY_TOKEN=... node scripts/smoke.mjs # live check, a few cents
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`test/fixtures/actor-inputs.json` lists the input fields of each published Actor; the tests fail if
|
|
96
|
+
a tool sends a field the Actor does not accept.
|
|
97
|
+
|
|
98
|
+
## Responsible use
|
|
99
|
+
|
|
100
|
+
The Actors read publicly available pages. You are responsible for following each site's terms and
|
|
101
|
+
the law that applies to you (GDPR, CAN-SPAM) when you use the data, especially contact details.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webdata-tools-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.tacps126/webdata-tools-mcp",
|
|
5
|
+
"description": "MCP server for AI agents: job postings, hiring signals, Google News, Trustpilot and app reviews, Reddit mentions, website contacts, German real estate and Shopify products. Runs ready-made Apify Actors with your own Apify token.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"webdata-tools-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/index.js",
|
|
18
|
+
"test": "node --test test/*.test.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"apify",
|
|
24
|
+
"web-scraping",
|
|
25
|
+
"jobs",
|
|
26
|
+
"hiring-signals",
|
|
27
|
+
"google-news",
|
|
28
|
+
"trustpilot",
|
|
29
|
+
"reviews",
|
|
30
|
+
"lead-generation"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/tacps126/webdata-tools-mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
42
|
+
"zod": "^4.6.7"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/tacps126/webdata-tools-mcp#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/tacps126/webdata-tools-mcp/issues"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/apify.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Thin Apify client: run one Actor synchronously and return its dataset items.
|
|
2
|
+
//
|
|
3
|
+
// Uses `POST /v2/acts/{owner}~{name}/run-sync-get-dataset-items`, which runs the Actor and
|
|
4
|
+
// returns the results in the same response (Apify caps this at 300 s). Billing happens on the
|
|
5
|
+
// caller's Apify account; `maxTotalChargeUsd` makes Apify stop the run at a spend ceiling, so a
|
|
6
|
+
// confused agent cannot run up a bill.
|
|
7
|
+
|
|
8
|
+
const DEFAULT_API_BASE = 'https://api.apify.com';
|
|
9
|
+
|
|
10
|
+
export class ApifyError extends Error {
|
|
11
|
+
constructor(message, status) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'ApifyError';
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} o
|
|
20
|
+
* @param {string} o.actor "owner/name"
|
|
21
|
+
* @param {object} o.input Actor input
|
|
22
|
+
* @param {number} o.limit max dataset items to return
|
|
23
|
+
* @param {string} o.token Apify API token
|
|
24
|
+
* @param {number} o.maxChargeUsd spend ceiling for this run
|
|
25
|
+
* @param {number} [o.timeoutSecs] Actor run timeout (<= 300)
|
|
26
|
+
* @param {typeof fetch} [o.fetchImpl]
|
|
27
|
+
* @param {string} [o.apiBase]
|
|
28
|
+
* @returns {Promise<{items: object[], runId: string|null}>}
|
|
29
|
+
*/
|
|
30
|
+
export async function runActorSync({
|
|
31
|
+
actor,
|
|
32
|
+
input,
|
|
33
|
+
limit,
|
|
34
|
+
token,
|
|
35
|
+
maxChargeUsd,
|
|
36
|
+
timeoutSecs = 150,
|
|
37
|
+
fetchImpl = fetch,
|
|
38
|
+
apiBase = process.env.APIFY_API_BASE || DEFAULT_API_BASE,
|
|
39
|
+
}) {
|
|
40
|
+
if (!token) {
|
|
41
|
+
throw new ApifyError(
|
|
42
|
+
'APIFY_TOKEN is not set. Create a free Apify account, copy your API token from ' +
|
|
43
|
+
'Console > Settings > API & Integrations, and pass it to this server as the ' +
|
|
44
|
+
'APIFY_TOKEN environment variable.',
|
|
45
|
+
401,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const [owner, name] = actor.split('/');
|
|
49
|
+
const url = new URL(`${apiBase}/v2/acts/${owner}~${name}/run-sync-get-dataset-items`);
|
|
50
|
+
url.searchParams.set('timeout', String(timeoutSecs));
|
|
51
|
+
url.searchParams.set('limit', String(limit));
|
|
52
|
+
url.searchParams.set('maxTotalChargeUsd', String(maxChargeUsd));
|
|
53
|
+
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await fetchImpl(url, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'content-type': 'application/json',
|
|
60
|
+
authorization: `Bearer ${token}`,
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(input),
|
|
63
|
+
signal: AbortSignal.timeout((timeoutSecs + 20) * 1000),
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new ApifyError(`Could not reach Apify: ${err.message}`, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const text = await res.text();
|
|
70
|
+
if (!res.ok) throw new ApifyError(describeFailure(res.status, text, actor), res.status);
|
|
71
|
+
|
|
72
|
+
let items;
|
|
73
|
+
try {
|
|
74
|
+
items = JSON.parse(text);
|
|
75
|
+
} catch {
|
|
76
|
+
throw new ApifyError('Apify returned a response that is not JSON.', res.status);
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(items)) items = [items];
|
|
79
|
+
return { items, runId: res.headers.get('x-apify-run-id') };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function describeFailure(status, body, actor) {
|
|
83
|
+
let detail = '';
|
|
84
|
+
try {
|
|
85
|
+
detail = JSON.parse(body)?.error?.message ?? '';
|
|
86
|
+
} catch {
|
|
87
|
+
detail = body.slice(0, 200);
|
|
88
|
+
}
|
|
89
|
+
switch (status) {
|
|
90
|
+
case 401:
|
|
91
|
+
return 'Apify rejected the API token. Check APIFY_TOKEN.';
|
|
92
|
+
case 402:
|
|
93
|
+
return `Not enough Apify credit to run ${actor}. ${detail}`.trim();
|
|
94
|
+
case 404:
|
|
95
|
+
return `Actor ${actor} was not found. ${detail}`.trim();
|
|
96
|
+
case 408:
|
|
97
|
+
return `${actor} did not finish in time. Try a smaller max_items. ${detail}`.trim();
|
|
98
|
+
case 429:
|
|
99
|
+
return 'Apify rate limit hit. Wait a few seconds and retry.';
|
|
100
|
+
default:
|
|
101
|
+
return `Apify error ${status}${detail ? `: ${detail}` : ''}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// webdata-tools-mcp: MCP server (stdio) that exposes ready-made Apify Actors as typed tools.
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { buildServer } from './server.js';
|
|
5
|
+
|
|
6
|
+
await buildServer().connect(new StdioServerTransport());
|
package/src/server.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// MCP server definition: one typed tool per Apify Actor.
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { ApifyError, runActorSync } from './apify.js';
|
|
5
|
+
import { extraInput, TOOLS } from './tools.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_CHARGE_USD = 0.5;
|
|
8
|
+
const MAX_TEXT_CHARS = 60_000; // keep one tool result inside a sane context budget
|
|
9
|
+
const MAX_FIELD_CHARS = 600;
|
|
10
|
+
|
|
11
|
+
/** Truncate very long string fields (job descriptions, review bodies) so results stay readable. */
|
|
12
|
+
export function shrink(value) {
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
return value.length > MAX_FIELD_CHARS ? `${value.slice(0, MAX_FIELD_CHARS)}…` : value;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) return value.map(shrink);
|
|
17
|
+
if (value && typeof value === 'object') {
|
|
18
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, shrink(v)]));
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Format dataset items as the text an agent reads, dropping rows if the result is too big. */
|
|
24
|
+
export function formatResult(tool, items) {
|
|
25
|
+
const rows = items.map(shrink);
|
|
26
|
+
let kept = rows.length;
|
|
27
|
+
let body = JSON.stringify(rows, null, 1);
|
|
28
|
+
while (body.length > MAX_TEXT_CHARS && kept > 1) {
|
|
29
|
+
kept = Math.max(1, Math.floor(kept * 0.7));
|
|
30
|
+
body = JSON.stringify(rows.slice(0, kept), null, 1);
|
|
31
|
+
}
|
|
32
|
+
const note =
|
|
33
|
+
kept < rows.length
|
|
34
|
+
? ` Showing the first ${kept} of ${rows.length} (output size limit); ask for fewer items.`
|
|
35
|
+
: '';
|
|
36
|
+
return `${tool.title}: ${rows.length} result(s).${note}\n${body}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildServer({ env = process.env, fetchImpl = fetch } = {}) {
|
|
40
|
+
const server = new McpServer({ name: 'webdata-tools-mcp', version: '0.1.0' });
|
|
41
|
+
const maxCharge = Number(env.WEBDATA_MAX_CHARGE_USD) || DEFAULT_MAX_CHARGE_USD;
|
|
42
|
+
|
|
43
|
+
for (const tool of TOOLS) {
|
|
44
|
+
server.registerTool(
|
|
45
|
+
tool.name,
|
|
46
|
+
{
|
|
47
|
+
title: tool.title,
|
|
48
|
+
description: `${tool.description} Runs the Apify Actor ${tool.actor} on your Apify account (${tool.price}; spend per call is capped at $${maxCharge}).`,
|
|
49
|
+
inputSchema: { ...tool.shape, extra_input: extraInput },
|
|
50
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
51
|
+
},
|
|
52
|
+
async (args) => {
|
|
53
|
+
const problem = tool.check?.(args);
|
|
54
|
+
if (problem) return { isError: true, content: [{ type: 'text', text: problem }] };
|
|
55
|
+
try {
|
|
56
|
+
const { items } = await runActorSync({
|
|
57
|
+
actor: tool.actor,
|
|
58
|
+
input: { ...tool.map(args), ...args.extra_input },
|
|
59
|
+
limit: tool.limit(args),
|
|
60
|
+
token: env.APIFY_TOKEN,
|
|
61
|
+
maxChargeUsd: maxCharge,
|
|
62
|
+
fetchImpl,
|
|
63
|
+
});
|
|
64
|
+
return { content: [{ type: 'text', text: formatResult(tool, items) }] };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const text = err instanceof ApifyError ? err.message : `Unexpected error: ${err.message}`;
|
|
67
|
+
return { isError: true, content: [{ type: 'text', text }] };
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return server;
|
|
73
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Tool catalogue. Each tool maps a small, agent-friendly argument set onto one Actor's input.
|
|
2
|
+
// `extra_input` lets an agent pass any other documented Actor input field through unchanged.
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const maxItems = (def, max = 500) =>
|
|
7
|
+
z
|
|
8
|
+
.number()
|
|
9
|
+
.int()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(max)
|
|
12
|
+
.default(def)
|
|
13
|
+
.describe(`Maximum results to return (default ${def}, max ${max}). Cost scales with this.`);
|
|
14
|
+
|
|
15
|
+
export const extraInput = z
|
|
16
|
+
.record(z.string(), z.unknown())
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Advanced: extra Actor input fields, merged over the defaults.');
|
|
19
|
+
|
|
20
|
+
const strings = (desc) => z.array(z.string().min(1)).min(1).describe(desc);
|
|
21
|
+
|
|
22
|
+
const clean = (obj) =>
|
|
23
|
+
Object.fromEntries(
|
|
24
|
+
Object.entries(obj).filter(
|
|
25
|
+
([, v]) => v !== undefined && v !== null && !(Array.isArray(v) && v.length === 0),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/** Each tool: name, actor, title, description, price note, input shape, map(args) -> Actor input. */
|
|
30
|
+
export const TOOLS = [
|
|
31
|
+
{
|
|
32
|
+
name: 'search_google_news',
|
|
33
|
+
actor: 'tacps126/google-news',
|
|
34
|
+
title: 'Search Google News',
|
|
35
|
+
price: 'about $0.002 per article',
|
|
36
|
+
description:
|
|
37
|
+
'Search Google News (any language and country) and return articles with the publisher\'s real article URL, publisher, date and related coverage. Supports Google operators ("exact phrase", -exclude, OR, intitle:) and site filters.',
|
|
38
|
+
shape: {
|
|
39
|
+
query: z.string().min(1).describe('Search query, e.g. "generative AI" or "tesla -musk".'),
|
|
40
|
+
country: z.string().length(2).default('US').describe('Two-letter edition country, e.g. US, GB, DE.'),
|
|
41
|
+
language: z.string().min(2).default('en').describe('Language code, e.g. en, de, fr.'),
|
|
42
|
+
timeframe: z.enum(['1h', '1d', '7d', '30d', '1y']).optional().describe('Only articles newer than this.'),
|
|
43
|
+
sites: z.array(z.string()).optional().describe('Limit to these publisher domains, e.g. ["reuters.com"].'),
|
|
44
|
+
max_items: maxItems(20, 200),
|
|
45
|
+
},
|
|
46
|
+
map: (a) =>
|
|
47
|
+
clean({
|
|
48
|
+
queries: [a.query],
|
|
49
|
+
country: a.country,
|
|
50
|
+
language: a.language,
|
|
51
|
+
timeframe: a.timeframe,
|
|
52
|
+
sites: a.sites,
|
|
53
|
+
maxItems: a.max_items,
|
|
54
|
+
}),
|
|
55
|
+
limit: (a) => a.max_items,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'find_hiring_companies',
|
|
59
|
+
actor: 'tacps126/hiring-signals',
|
|
60
|
+
title: 'Find companies hiring for given roles',
|
|
61
|
+
price: 'about $0.01 per company with a matching signal; companies without one are free',
|
|
62
|
+
description:
|
|
63
|
+
'Buying-intent signal: which companies are hiring for the roles you care about, how many roles, how fresh, and seniority/locations. Give target accounts (their jobs-page URLs on Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Recruitee, Personio, Teamtailor) and/or job-search phrases to discover companies.',
|
|
64
|
+
shape: {
|
|
65
|
+
role_keywords: z
|
|
66
|
+
.array(z.string())
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('A role counts when its title contains any of these words, e.g. ["data engineer","security"]. Empty = every role.'),
|
|
69
|
+
companies: z
|
|
70
|
+
.array(z.string())
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('Target accounts\' jobs-page URLs, e.g. "https://boards.greenhouse.io/stripe".'),
|
|
73
|
+
searches: z
|
|
74
|
+
.array(z.string())
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Job-search phrases to discover companies you do not know yet, e.g. "head of security".'),
|
|
77
|
+
locations: z.array(z.string()).optional().describe('Keep roles in these locations.'),
|
|
78
|
+
posted_within_days: z.number().int().min(1).max(365).default(30).describe('Only roles posted in the last N days.'),
|
|
79
|
+
max_items: maxItems(50, 500),
|
|
80
|
+
},
|
|
81
|
+
check: (a) =>
|
|
82
|
+
(a.companies?.length || a.searches?.length)
|
|
83
|
+
? null
|
|
84
|
+
: 'Provide at least one of: companies (jobs-page URLs) or searches (job-search phrases).',
|
|
85
|
+
map: (a) =>
|
|
86
|
+
clean({
|
|
87
|
+
companies: a.companies,
|
|
88
|
+
searches: a.searches,
|
|
89
|
+
roleKeywords: a.role_keywords,
|
|
90
|
+
locations: a.locations,
|
|
91
|
+
postedWithinDays: a.posted_within_days,
|
|
92
|
+
maxItems: a.max_items,
|
|
93
|
+
}),
|
|
94
|
+
limit: (a) => a.max_items,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'get_company_jobs',
|
|
98
|
+
actor: 'tacps126/jobs-harvester',
|
|
99
|
+
title: 'Get all open jobs from company career pages',
|
|
100
|
+
price: 'about $0.001 per job',
|
|
101
|
+
description:
|
|
102
|
+
'All open jobs from company career pages in one normalized format (title, company, location, remote/hybrid, seniority, salary, apply URL, posted date). Paste career-page URLs from Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Workday, Personio or Teamtailor; the board type is detected automatically.',
|
|
103
|
+
shape: {
|
|
104
|
+
career_page_urls: strings('Career page URLs, e.g. "https://jobs.lever.co/spotify".'),
|
|
105
|
+
title_includes: z.array(z.string()).optional().describe('Keep jobs whose title contains any of these.'),
|
|
106
|
+
location_includes: z.array(z.string()).optional().describe('Keep jobs in these locations.'),
|
|
107
|
+
remote_only: z.boolean().optional().describe('Only remote jobs.'),
|
|
108
|
+
posted_within_days: z.number().int().min(1).max(365).optional().describe('Only jobs posted in the last N days.'),
|
|
109
|
+
max_items: maxItems(50),
|
|
110
|
+
},
|
|
111
|
+
map: (a) =>
|
|
112
|
+
clean({
|
|
113
|
+
careerPageUrls: a.career_page_urls,
|
|
114
|
+
titleIncludes: a.title_includes,
|
|
115
|
+
locationIncludes: a.location_includes,
|
|
116
|
+
remoteOnly: a.remote_only,
|
|
117
|
+
postedWithinDays: a.posted_within_days,
|
|
118
|
+
maxItems: a.max_items,
|
|
119
|
+
}),
|
|
120
|
+
limit: (a) => a.max_items,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'search_indeed_jobs',
|
|
124
|
+
actor: 'tacps126/indeed-jobs',
|
|
125
|
+
title: 'Search Indeed jobs',
|
|
126
|
+
price: 'about $0.003 per job',
|
|
127
|
+
description:
|
|
128
|
+
'Search Indeed job listings by keyword and location in 9 countries (us, uk, ca, au, ie, de, fr, nl, sg): title, company, location, salary, job type, benefits, skills and apply link.',
|
|
129
|
+
shape: {
|
|
130
|
+
query: z.string().min(1).describe('Job title, skill or company, e.g. "react developer".'),
|
|
131
|
+
location: z.string().optional().describe('City, state, postcode or "Remote". Empty = whole country.'),
|
|
132
|
+
country: z.enum(['us', 'uk', 'ca', 'au', 'ie', 'de', 'fr', 'nl', 'sg']).default('us').describe('Indeed country site.'),
|
|
133
|
+
date_posted_days: z.enum(['1', '3', '7', '14']).optional().describe('Only jobs posted in the last N days.'),
|
|
134
|
+
job_type: z.enum(['fulltime', 'parttime', 'contract', 'temporary', 'internship']).optional(),
|
|
135
|
+
max_items: maxItems(30),
|
|
136
|
+
},
|
|
137
|
+
map: (a) =>
|
|
138
|
+
clean({
|
|
139
|
+
queries: [a.query],
|
|
140
|
+
locations: a.location ? [a.location] : undefined,
|
|
141
|
+
country: a.country,
|
|
142
|
+
datePosted: a.date_posted_days,
|
|
143
|
+
jobType: a.job_type,
|
|
144
|
+
maxItems: a.max_items,
|
|
145
|
+
}),
|
|
146
|
+
limit: (a) => a.max_items,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'get_trustpilot_reviews',
|
|
150
|
+
actor: 'tacps126/trustpilot-reviews',
|
|
151
|
+
title: 'Get Trustpilot reviews and company rating',
|
|
152
|
+
price: 'about $0.0005 per review; company profile free',
|
|
153
|
+
description:
|
|
154
|
+
'Trustpilot reviews for one or more companies (rating, title, text, reviewer, country, date, company reply) plus the company\'s TrustScore and review counts. Filter by stars, language, verified and date period.',
|
|
155
|
+
shape: {
|
|
156
|
+
companies: strings('Company domains or Trustpilot URLs, e.g. "apify.com".'),
|
|
157
|
+
star_ratings: z.array(z.enum(['1', '2', '3', '4', '5'])).optional().describe('Only these ratings, e.g. ["1","2"] for complaints.'),
|
|
158
|
+
sort: z.enum(['recency', 'relevance']).default('recency'),
|
|
159
|
+
search: z.string().optional().describe('Only reviews containing this text.'),
|
|
160
|
+
date_period: z.enum(['last30days', 'last3months', 'last6months']).optional(),
|
|
161
|
+
max_reviews: maxItems(50),
|
|
162
|
+
},
|
|
163
|
+
map: (a) =>
|
|
164
|
+
clean({
|
|
165
|
+
companies: a.companies,
|
|
166
|
+
starRatings: a.star_ratings,
|
|
167
|
+
sort: a.sort,
|
|
168
|
+
search: a.search,
|
|
169
|
+
datePeriod: a.date_period,
|
|
170
|
+
maxReviews: a.max_reviews,
|
|
171
|
+
}),
|
|
172
|
+
limit: (a) => a.max_reviews + a.companies.length,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'get_app_reviews',
|
|
176
|
+
actor: 'tacps126/app-reviews',
|
|
177
|
+
title: 'Get App Store and Google Play reviews',
|
|
178
|
+
price: 'about $0.0002 per review',
|
|
179
|
+
description:
|
|
180
|
+
'Reviews from the Apple App Store and Google Play in one dataset: rating, text, author, date, app version, developer reply, sentiment. Mix both stores and several countries.',
|
|
181
|
+
shape: {
|
|
182
|
+
apps: strings('App Store / Google Play links, App Store ids ("id324684580") or Android package names ("com.spotify.music").'),
|
|
183
|
+
countries: z.array(z.string().length(2)).default(['us']).describe('Two-letter store countries, e.g. ["us","gb"].'),
|
|
184
|
+
star_ratings: z.array(z.enum(['1', '2', '3', '4', '5'])).optional().describe('Only these ratings, e.g. ["1","2"].'),
|
|
185
|
+
max_items: maxItems(100, 2000),
|
|
186
|
+
},
|
|
187
|
+
map: (a) =>
|
|
188
|
+
clean({
|
|
189
|
+
apps: a.apps,
|
|
190
|
+
countries: a.countries,
|
|
191
|
+
starRatings: a.star_ratings,
|
|
192
|
+
maxItems: a.max_items,
|
|
193
|
+
maxReviewsPerApp: a.max_items,
|
|
194
|
+
}),
|
|
195
|
+
limit: (a) => a.max_items,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'search_reddit',
|
|
199
|
+
actor: 'tacps126/reddit-monitor',
|
|
200
|
+
title: 'Search Reddit posts and comments',
|
|
201
|
+
price: 'about $0.001 per post or comment',
|
|
202
|
+
description:
|
|
203
|
+
'Reddit posts (and optionally comments) by keyword, subreddit or user: title, text, author, subreddit, score, links and dates. Good for brand and topic monitoring.',
|
|
204
|
+
shape: {
|
|
205
|
+
keywords: z.array(z.string()).optional().describe('Search all of Reddit; "exact phrase" works.'),
|
|
206
|
+
subreddits: z.array(z.string()).optional().describe('Subreddit names, e.g. ["webscraping"].'),
|
|
207
|
+
sort: z.enum(['new', 'hot', 'top', 'relevance']).default('new'),
|
|
208
|
+
time: z.enum(['hour', 'day', 'week', 'month', 'year', 'all']).default('week'),
|
|
209
|
+
include_comments: z.boolean().default(false),
|
|
210
|
+
max_items: maxItems(50),
|
|
211
|
+
},
|
|
212
|
+
check: (a) =>
|
|
213
|
+
(a.keywords?.length || a.subreddits?.length)
|
|
214
|
+
? null
|
|
215
|
+
: 'Provide at least one of: keywords or subreddits.',
|
|
216
|
+
map: (a) =>
|
|
217
|
+
clean({
|
|
218
|
+
keywords: a.keywords,
|
|
219
|
+
subreddits: a.subreddits,
|
|
220
|
+
sort: a.sort,
|
|
221
|
+
time: a.time,
|
|
222
|
+
includeComments: a.include_comments,
|
|
223
|
+
maxItems: a.max_items,
|
|
224
|
+
}),
|
|
225
|
+
limit: (a) => a.max_items,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'find_website_contacts',
|
|
229
|
+
actor: 'tacps126/contact-details',
|
|
230
|
+
title: 'Find emails, phones and social profiles on websites',
|
|
231
|
+
price: 'about $0.002 per website with contacts found; nothing found, no charge',
|
|
232
|
+
description:
|
|
233
|
+
'Finds business emails, phone numbers, addresses and social profiles (LinkedIn, X, Facebook, Instagram, YouTube) on any list of websites; it visits the contact and about pages itself. Use only for lawful business outreach (GDPR / CAN-SPAM).',
|
|
234
|
+
shape: {
|
|
235
|
+
websites: strings('Website addresses or domains, e.g. "apify.com".'),
|
|
236
|
+
max_pages_per_site: z.number().int().min(1).max(20).default(5),
|
|
237
|
+
require_email: z.boolean().default(false).describe('Drop sites where no email was found.'),
|
|
238
|
+
max_items: maxItems(50, 1000),
|
|
239
|
+
},
|
|
240
|
+
map: (a) =>
|
|
241
|
+
clean({
|
|
242
|
+
startUrls: a.websites,
|
|
243
|
+
maxPagesPerSite: a.max_pages_per_site,
|
|
244
|
+
requireEmail: a.require_email,
|
|
245
|
+
maxItems: a.max_items,
|
|
246
|
+
}),
|
|
247
|
+
limit: (a) => a.max_items,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'search_german_property_listings',
|
|
251
|
+
actor: 'tacps126/immobilienscout24-scraper',
|
|
252
|
+
title: 'Search German property listings (ImmoScout24)',
|
|
253
|
+
price: 'about $0.001 per listing',
|
|
254
|
+
description:
|
|
255
|
+
'Apartments and houses for rent or sale in Germany: price, rent, area, rooms, address with coordinates, energy class, features, agent, photos and dates. Germany only.',
|
|
256
|
+
shape: {
|
|
257
|
+
locations: strings('Cities, districts or regions in plain words, e.g. ["Berlin","München Schwabing"].'),
|
|
258
|
+
offer_type: z.enum(['rent', 'buy']).default('rent'),
|
|
259
|
+
property_type: z.enum(['apartment', 'house']).default('apartment'),
|
|
260
|
+
min_price: z.number().optional(),
|
|
261
|
+
max_price: z.number().optional(),
|
|
262
|
+
min_area: z.number().optional().describe('Minimum area in m2.'),
|
|
263
|
+
min_rooms: z.number().optional(),
|
|
264
|
+
max_items: maxItems(50),
|
|
265
|
+
},
|
|
266
|
+
map: (a) =>
|
|
267
|
+
clean({
|
|
268
|
+
locations: a.locations,
|
|
269
|
+
offerType: a.offer_type,
|
|
270
|
+
propertyType: a.property_type,
|
|
271
|
+
minPrice: a.min_price,
|
|
272
|
+
maxPrice: a.max_price,
|
|
273
|
+
minArea: a.min_area,
|
|
274
|
+
minRooms: a.min_rooms,
|
|
275
|
+
maxItems: a.max_items,
|
|
276
|
+
}),
|
|
277
|
+
limit: (a) => a.max_items,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'get_shopify_products',
|
|
281
|
+
actor: 'tacps126/shopify-products',
|
|
282
|
+
title: 'Get products from Shopify stores',
|
|
283
|
+
price: 'about $0.001 per product',
|
|
284
|
+
description:
|
|
285
|
+
'Products from any Shopify store: prices, sale prices, discounts, stock, variants, SKUs, images and descriptions. Whole stores, collections or single products. Useful for price and stock monitoring.',
|
|
286
|
+
shape: {
|
|
287
|
+
stores: strings('Store domains, collection links or product links, e.g. "allbirds.com".'),
|
|
288
|
+
keywords: z.array(z.string()).optional().describe('Only products matching any of these words.'),
|
|
289
|
+
only_on_sale: z.boolean().default(false),
|
|
290
|
+
only_available: z.boolean().default(false),
|
|
291
|
+
max_items: maxItems(100, 1000),
|
|
292
|
+
},
|
|
293
|
+
map: (a) =>
|
|
294
|
+
clean({
|
|
295
|
+
startUrls: a.stores,
|
|
296
|
+
keywords: a.keywords,
|
|
297
|
+
onlyOnSale: a.only_on_sale || undefined,
|
|
298
|
+
onlyAvailable: a.only_available || undefined,
|
|
299
|
+
maxItems: a.max_items,
|
|
300
|
+
}),
|
|
301
|
+
limit: (a) => a.max_items,
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
export const ACTORS = [...new Set(TOOLS.map((t) => t.actor))];
|