webdata-tools-mcp 0.1.0 → 0.4.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 +22 -3
- package/icon.png +0 -0
- package/package.json +6 -3
- package/src/server.js +43 -10
- package/src/tools.js +42 -17
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# webdata-tools-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/webdata-tools-mcp)
|
|
4
|
+
[](https://github.com/tacps126/webdata-tools-mcp/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[Smithery](https://smithery.ai/servers/tacps126/webdata-tools-mcp)
|
|
7
|
+
[](https://glama.ai/mcp/servers/tacps126/webdata-tools-mcp)
|
|
8
|
+
|
|
9
|
+
An [MCP](https://modelcontextprotocol.io) server that gives AI agents eleven web-data tools: job
|
|
4
10
|
postings, hiring signals, Google News, Trustpilot and app-store reviews, Reddit mentions, website
|
|
5
|
-
contacts, German property listings and
|
|
11
|
+
contacts, German and Austrian property listings, Shopify products and trade-show exhibitor lists.
|
|
6
12
|
|
|
7
13
|
Each tool runs a ready-made [Apify](https://apify.com/tacps126) Actor and returns clean JSON. You
|
|
8
14
|
pay Apify per result (about $1–3 per 1,000 results); there is no subscription and nothing is sent
|
|
@@ -20,10 +26,15 @@ anywhere except Apify.
|
|
|
20
26
|
| `get_app_reviews` | App Store and Google Play reviews | $0.20 / 1,000 reviews | [app-reviews](https://apify.com/tacps126/app-reviews) |
|
|
21
27
|
| `search_reddit` | Reddit posts and comments by keyword or subreddit | $1 / 1,000 items | [reddit-monitor](https://apify.com/tacps126/reddit-monitor) |
|
|
22
28
|
| `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) |
|
|
29
|
+
| `search_german_property_listings` | Rent/buy listings in Germany and Austria (ImmoScout24) | $1 / 1,000 listings | [immobilienscout24-scraper](https://apify.com/tacps126/immobilienscout24-scraper) |
|
|
24
30
|
| `get_shopify_products` | Prices, discounts, stock, variants from Shopify stores | $1 / 1,000 products | [shopify-products](https://apify.com/tacps126/shopify-products) |
|
|
31
|
+
| `get_trade_show_exhibitors` | Trade-show exhibitors: booth, email, phone, website, address (76 shows or any supported exhibitor-list link) | $2 / 1,000 exhibitors | [exhibition-scraper](https://apify.com/tacps126/exhibition-scraper) |
|
|
25
32
|
|
|
26
33
|
Every tool also takes an optional `extra_input` object for any other field the Actor documents.
|
|
34
|
+
Every result comes back as text and as structured content (`tool`, `count`, `shown`, `truncated`,
|
|
35
|
+
`items`), and every tool is marked read-only, so clients can run it without asking.
|
|
36
|
+
|
|
37
|
+
More about the tools: <https://tacps126.github.io/mcp.html>
|
|
27
38
|
|
|
28
39
|
## Setup
|
|
29
40
|
|
|
@@ -60,6 +71,14 @@ Requires Node.js 18 or newer.
|
|
|
60
71
|
- "Find news from the last 7 days about open-source LLM licensing, with the real publisher links."
|
|
61
72
|
- "Get contact emails for these 20 agency websites." (use only for lawful business outreach)
|
|
62
73
|
|
|
74
|
+
## Settings
|
|
75
|
+
|
|
76
|
+
| Environment variable | Default | Meaning |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `APIFY_TOKEN` | required | Your Apify API token |
|
|
79
|
+
| `WEBDATA_MAX_CHARGE_USD` | `0.5` | Spend cap per tool call; Apify stops the run when it is reached |
|
|
80
|
+
| `WEBDATA_TIMEOUT_SECS` | `150` | How long one call may run (30 to 300) |
|
|
81
|
+
|
|
63
82
|
## Cost safety
|
|
64
83
|
|
|
65
84
|
- Every call is capped at **$0.50** of Apify spend (`WEBDATA_MAX_CHARGE_USD` changes it). Apify stops
|
package/icon.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webdata-tools-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
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
|
|
5
|
+
"description": "MCP server for AI agents: job postings, hiring signals, Google News, Trustpilot and app reviews, Reddit mentions, website contacts, German real estate, Shopify products and trade-show exhibitor lists. Runs ready-made Apify Actors with your own Apify token.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"webdata-tools-mcp": "src/index.js"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"main": "src/index.js",
|
|
11
11
|
"files": [
|
|
12
12
|
"src",
|
|
13
|
+
"icon.png",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
@@ -27,7 +28,9 @@
|
|
|
27
28
|
"google-news",
|
|
28
29
|
"trustpilot",
|
|
29
30
|
"reviews",
|
|
30
|
-
"lead-generation"
|
|
31
|
+
"lead-generation",
|
|
32
|
+
"trade-shows",
|
|
33
|
+
"exhibitors"
|
|
31
34
|
],
|
|
32
35
|
"license": "MIT",
|
|
33
36
|
"engines": {
|
package/src/server.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// MCP server definition: one typed tool per Apify Actor.
|
|
2
2
|
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { z } from 'zod';
|
|
4
6
|
import { ApifyError, runActorSync } from './apify.js';
|
|
5
7
|
import { extraInput, TOOLS } from './tools.js';
|
|
6
8
|
|
|
9
|
+
const { version: VERSION } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
10
|
+
|
|
7
11
|
const DEFAULT_MAX_CHARGE_USD = 0.5;
|
|
12
|
+
const DEFAULT_TIMEOUT_SECS = 150;
|
|
8
13
|
const MAX_TEXT_CHARS = 60_000; // keep one tool result inside a sane context budget
|
|
9
14
|
const MAX_FIELD_CHARS = 600;
|
|
10
15
|
|
|
@@ -20,8 +25,26 @@ export function shrink(value) {
|
|
|
20
25
|
return value;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
/**
|
|
24
|
-
export
|
|
28
|
+
/** Shape of every successful tool result (also advertised as the tool's output schema). */
|
|
29
|
+
export const outputShape = {
|
|
30
|
+
tool: z.string().describe('Name of the tool that produced the result.'),
|
|
31
|
+
count: z.number().int().describe('Number of records the Actor returned.'),
|
|
32
|
+
shown: z.number().int().describe('Number of records included in `items` (can be lower than count when the result is long).'),
|
|
33
|
+
truncated: z.boolean().describe('True when records were left out to keep the result short; ask for fewer items.'),
|
|
34
|
+
items: z.array(z.record(z.string(), z.unknown())).describe('The records. Long text fields are shortened to 600 characters.'),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** MCP annotations: every tool only reads public web data, so it is read-only and repeatable. */
|
|
38
|
+
export const annotationsOf = (tool) => ({
|
|
39
|
+
title: tool.title,
|
|
40
|
+
readOnlyHint: true,
|
|
41
|
+
destructiveHint: false,
|
|
42
|
+
idempotentHint: true,
|
|
43
|
+
openWorldHint: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Turn dataset items into the text an agent reads plus the structured result, dropping rows if too big. */
|
|
47
|
+
export function buildResult(tool, items) {
|
|
25
48
|
const rows = items.map(shrink);
|
|
26
49
|
let kept = rows.length;
|
|
27
50
|
let body = JSON.stringify(rows, null, 1);
|
|
@@ -29,16 +52,23 @@ export function formatResult(tool, items) {
|
|
|
29
52
|
kept = Math.max(1, Math.floor(kept * 0.7));
|
|
30
53
|
body = JSON.stringify(rows.slice(0, kept), null, 1);
|
|
31
54
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
55
|
+
const truncated = kept < rows.length;
|
|
56
|
+
const note = truncated
|
|
57
|
+
? ` Showing the first ${kept} of ${rows.length} (output size limit); ask for fewer items.`
|
|
58
|
+
: '';
|
|
59
|
+
return {
|
|
60
|
+
text: `${tool.title}: ${rows.length} result(s).${note}\n${body}`,
|
|
61
|
+
structured: { tool: tool.name, count: rows.length, shown: kept, truncated, items: rows.slice(0, kept) },
|
|
62
|
+
};
|
|
37
63
|
}
|
|
38
64
|
|
|
65
|
+
/** Same as buildResult but only the text (kept for callers that do not use structured output). */
|
|
66
|
+
export const formatResult = (tool, items) => buildResult(tool, items).text;
|
|
67
|
+
|
|
39
68
|
export function buildServer({ env = process.env, fetchImpl = fetch } = {}) {
|
|
40
|
-
const server = new McpServer({ name: 'webdata-tools-mcp', version:
|
|
69
|
+
const server = new McpServer({ name: 'webdata-tools-mcp', version: VERSION });
|
|
41
70
|
const maxCharge = Number(env.WEBDATA_MAX_CHARGE_USD) || DEFAULT_MAX_CHARGE_USD;
|
|
71
|
+
const timeoutSecs = Math.min(300, Math.max(30, Number(env.WEBDATA_TIMEOUT_SECS) || DEFAULT_TIMEOUT_SECS));
|
|
42
72
|
|
|
43
73
|
for (const tool of TOOLS) {
|
|
44
74
|
server.registerTool(
|
|
@@ -47,7 +77,8 @@ export function buildServer({ env = process.env, fetchImpl = fetch } = {}) {
|
|
|
47
77
|
title: tool.title,
|
|
48
78
|
description: `${tool.description} Runs the Apify Actor ${tool.actor} on your Apify account (${tool.price}; spend per call is capped at $${maxCharge}).`,
|
|
49
79
|
inputSchema: { ...tool.shape, extra_input: extraInput },
|
|
50
|
-
|
|
80
|
+
outputSchema: outputShape,
|
|
81
|
+
annotations: annotationsOf(tool),
|
|
51
82
|
},
|
|
52
83
|
async (args) => {
|
|
53
84
|
const problem = tool.check?.(args);
|
|
@@ -59,9 +90,11 @@ export function buildServer({ env = process.env, fetchImpl = fetch } = {}) {
|
|
|
59
90
|
limit: tool.limit(args),
|
|
60
91
|
token: env.APIFY_TOKEN,
|
|
61
92
|
maxChargeUsd: maxCharge,
|
|
93
|
+
timeoutSecs,
|
|
62
94
|
fetchImpl,
|
|
63
95
|
});
|
|
64
|
-
|
|
96
|
+
const { text, structured } = buildResult(tool, items);
|
|
97
|
+
return { content: [{ type: 'text', text }], structuredContent: structured };
|
|
65
98
|
} catch (err) {
|
|
66
99
|
const text = err instanceof ApifyError ? err.message : `Unexpected error: ${err.message}`;
|
|
67
100
|
return { isError: true, content: [{ type: 'text', text }] };
|
package/src/tools.js
CHANGED
|
@@ -131,7 +131,7 @@ export const TOOLS = [
|
|
|
131
131
|
location: z.string().optional().describe('City, state, postcode or "Remote". Empty = whole country.'),
|
|
132
132
|
country: z.enum(['us', 'uk', 'ca', 'au', 'ie', 'de', 'fr', 'nl', 'sg']).default('us').describe('Indeed country site.'),
|
|
133
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(),
|
|
134
|
+
job_type: z.enum(['fulltime', 'parttime', 'contract', 'temporary', 'internship']).optional().describe('Only this employment type.'),
|
|
135
135
|
max_items: maxItems(30),
|
|
136
136
|
},
|
|
137
137
|
map: (a) =>
|
|
@@ -155,9 +155,9 @@ export const TOOLS = [
|
|
|
155
155
|
shape: {
|
|
156
156
|
companies: strings('Company domains or Trustpilot URLs, e.g. "apify.com".'),
|
|
157
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'),
|
|
158
|
+
sort: z.enum(['recency', 'relevance']).default('recency').describe('Newest reviews first (recency) or most relevant first.'),
|
|
159
159
|
search: z.string().optional().describe('Only reviews containing this text.'),
|
|
160
|
-
date_period: z.enum(['last30days', 'last3months', 'last6months']).optional(),
|
|
160
|
+
date_period: z.enum(['last30days', 'last3months', 'last6months']).optional().describe('Only reviews from this period.'),
|
|
161
161
|
max_reviews: maxItems(50),
|
|
162
162
|
},
|
|
163
163
|
map: (a) =>
|
|
@@ -204,9 +204,9 @@ export const TOOLS = [
|
|
|
204
204
|
shape: {
|
|
205
205
|
keywords: z.array(z.string()).optional().describe('Search all of Reddit; "exact phrase" works.'),
|
|
206
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),
|
|
207
|
+
sort: z.enum(['new', 'hot', 'top', 'relevance']).default('new').describe('Result order.'),
|
|
208
|
+
time: z.enum(['hour', 'day', 'week', 'month', 'year', 'all']).default('week').describe('Time window for keyword searches.'),
|
|
209
|
+
include_comments: z.boolean().default(false).describe('Also return comments of the matched posts (more results, higher cost).'),
|
|
210
210
|
max_items: maxItems(50),
|
|
211
211
|
},
|
|
212
212
|
check: (a) =>
|
|
@@ -233,7 +233,7 @@ export const TOOLS = [
|
|
|
233
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
234
|
shape: {
|
|
235
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),
|
|
236
|
+
max_pages_per_site: z.number().int().min(1).max(20).default(5).describe('Pages read per website; contact, imprint and about pages are picked first.'),
|
|
237
237
|
require_email: z.boolean().default(false).describe('Drop sites where no email was found.'),
|
|
238
238
|
max_items: maxItems(50, 1000),
|
|
239
239
|
},
|
|
@@ -249,22 +249,24 @@ export const TOOLS = [
|
|
|
249
249
|
{
|
|
250
250
|
name: 'search_german_property_listings',
|
|
251
251
|
actor: 'tacps126/immobilienscout24-scraper',
|
|
252
|
-
title: 'Search German property listings (ImmoScout24)',
|
|
252
|
+
title: 'Search German and Austrian property listings (ImmoScout24)',
|
|
253
253
|
price: 'about $0.001 per listing',
|
|
254
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.
|
|
255
|
+
'Apartments and houses for rent or sale in Germany or Austria: price, rent, area, rooms, address with coordinates, energy class, features, agent, photos and dates. Not Switzerland.',
|
|
256
256
|
shape: {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
country: z.enum(['de', 'at']).default('de').describe('de = Germany (immobilienscout24.de), at = Austria (immobilienscout24.at).'),
|
|
258
|
+
locations: strings('Cities, districts or regions in plain words, e.g. ["Berlin","München Schwabing"]; in Austria a city or federal state, e.g. ["Wien","Tirol"].'),
|
|
259
|
+
offer_type: z.enum(['rent', 'buy']).default('rent').describe('Rent or buy listings.'),
|
|
260
|
+
property_type: z.enum(['apartment', 'house']).default('apartment').describe('Apartments or houses.'),
|
|
261
|
+
min_price: z.number().optional().describe('Minimum price in EUR (monthly rent or purchase price).'),
|
|
262
|
+
max_price: z.number().optional().describe('Maximum price in EUR (monthly rent or purchase price).'),
|
|
262
263
|
min_area: z.number().optional().describe('Minimum area in m2.'),
|
|
263
|
-
min_rooms: z.number().optional(),
|
|
264
|
+
min_rooms: z.number().optional().describe('Minimum number of rooms.'),
|
|
264
265
|
max_items: maxItems(50),
|
|
265
266
|
},
|
|
266
267
|
map: (a) =>
|
|
267
268
|
clean({
|
|
269
|
+
country: a.country,
|
|
268
270
|
locations: a.locations,
|
|
269
271
|
offerType: a.offer_type,
|
|
270
272
|
propertyType: a.property_type,
|
|
@@ -286,8 +288,8 @@ export const TOOLS = [
|
|
|
286
288
|
shape: {
|
|
287
289
|
stores: strings('Store domains, collection links or product links, e.g. "allbirds.com".'),
|
|
288
290
|
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
|
+
only_on_sale: z.boolean().default(false).describe('Only products with a sale price.'),
|
|
292
|
+
only_available: z.boolean().default(false).describe('Only products that are in stock.'),
|
|
291
293
|
max_items: maxItems(100, 1000),
|
|
292
294
|
},
|
|
293
295
|
map: (a) =>
|
|
@@ -300,6 +302,29 @@ export const TOOLS = [
|
|
|
300
302
|
}),
|
|
301
303
|
limit: (a) => a.max_items,
|
|
302
304
|
},
|
|
305
|
+
{
|
|
306
|
+
name: 'get_trade_show_exhibitors',
|
|
307
|
+
actor: 'tacps126/exhibition-scraper',
|
|
308
|
+
title: 'Get trade show exhibitor lists (booths and contacts)',
|
|
309
|
+
price: 'about $0.002 per exhibitor',
|
|
310
|
+
description:
|
|
311
|
+
"Exhibitor lists of trade shows as B2B leads: company, booth and hall, email, phone, website, address, socials and categories. 76 built-in shows (e.g. medica, sema-show, nab-show, fabtech, ish, automechanika, wtm-london, world-health-expo) or any exhibitor-list link on Messe Frankfurt, Messe Düsseldorf, RX, Map Your Show, a2z / A2Z Connect, Swapcard or Corussoft; the link's own show and year are used. Contact coverage depends on what the organiser publishes.",
|
|
312
|
+
shape: {
|
|
313
|
+
shows: z.array(z.string().min(1)).optional().describe('Built-in show ids, e.g. ["medica","sema-show"]. The Actor README lists all ids.'),
|
|
314
|
+
urls: z.array(z.string().url()).optional().describe("Links to any show's exhibitor list or exhibitor search page (or its homepage)."),
|
|
315
|
+
include_contacts: z.boolean().default(true).describe('Open each exhibitor profile for email, phone, website, address and socials. Off = names and booths only (faster).'),
|
|
316
|
+
max_items: maxItems(50, 2000),
|
|
317
|
+
},
|
|
318
|
+
map: (a) =>
|
|
319
|
+
clean({
|
|
320
|
+
exhibitionList: a.shows,
|
|
321
|
+
startUrls: (a.urls ?? []).map((url) => ({ url })),
|
|
322
|
+
includeDetails: a.include_contacts,
|
|
323
|
+
maxResults: a.max_items,
|
|
324
|
+
}),
|
|
325
|
+
// max_items applies per show.
|
|
326
|
+
limit: (a) => a.max_items * Math.max(1, (a.shows?.length ?? 0) + (a.urls?.length ?? 0)),
|
|
327
|
+
},
|
|
303
328
|
];
|
|
304
329
|
|
|
305
330
|
export const ACTORS = [...new Set(TOOLS.map((t) => t.actor))];
|