@fouradata/mcp 0.2.11
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 +258 -0
- package/bin/foura-mcp.js +2 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +32 -0
- package/dist/auth.js.map +1 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +182 -0
- package/dist/http.js.map +1 -0
- package/dist/prompts.d.ts +11 -0
- package/dist/prompts.js +149 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +33 -0
- package/dist/resources.js +126 -0
- package/dist/resources.js.map +1 -0
- package/dist/safe-target.d.ts +5 -0
- package/dist/safe-target.js +130 -0
- package/dist/safe-target.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +19 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +7 -0
- package/dist/stdio.js.map +1 -0
- package/dist/tools/browser.d.ts +77 -0
- package/dist/tools/browser.js +316 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/proxy.d.ts +113 -0
- package/dist/tools/proxy.js +379 -0
- package/dist/tools/proxy.js.map +1 -0
- package/dist/tools/single.d.ts +79 -0
- package/dist/tools/single.js +374 -0
- package/dist/tools/single.js.map +1 -0
- package/package.json +63 -0
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* MCP Prompts — pre-written workflow templates the user can invoke from the
|
|
4
|
+
* MCP client UI (Claude Desktop / Cursor / etc) instead of figuring out the
|
|
5
|
+
* tool orchestration themselves.
|
|
6
|
+
*
|
|
7
|
+
* Prompts are LAZY context — they only enter the LLM's window when invoked,
|
|
8
|
+
* unlike tool descriptions which are loaded on every turn. So we can be more
|
|
9
|
+
* verbose here.
|
|
10
|
+
*/
|
|
11
|
+
export function registerPrompts(server) {
|
|
12
|
+
server.registerPrompt("scrape_product_page", {
|
|
13
|
+
title: "Scrape a product page",
|
|
14
|
+
description: "Fetch an e-commerce product URL and extract structured product info as JSON (title, price, image, availability).",
|
|
15
|
+
argsSchema: {
|
|
16
|
+
url: z.string().describe("Product page URL (any e-commerce site)"),
|
|
17
|
+
},
|
|
18
|
+
}, ({ url }) => ({
|
|
19
|
+
messages: [
|
|
20
|
+
{
|
|
21
|
+
role: "user",
|
|
22
|
+
content: {
|
|
23
|
+
type: "text",
|
|
24
|
+
text: `Fetch the product page at ${url} using the foura_browser tool — most product pages are single-page apps and need JavaScript to render.\n\n` +
|
|
25
|
+
`From the response body extract:\n` +
|
|
26
|
+
`- product title\n` +
|
|
27
|
+
`- price (with currency)\n` +
|
|
28
|
+
`- primary product image URL (absolute, not relative)\n` +
|
|
29
|
+
`- availability / stock status\n` +
|
|
30
|
+
`- product SKU or ID if visible\n\n` +
|
|
31
|
+
`If the response body is large (you'll see a resource_link instead of inline HTML), call resources/read on that URI to get the full page.\n\n` +
|
|
32
|
+
`Return the result as JSON:\n` +
|
|
33
|
+
`{"title": "...", "price": 0, "currency": "USD", "image_url": "...", "in_stock": true, "sku": "..."}`,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}));
|
|
38
|
+
server.registerPrompt("extract_article", {
|
|
39
|
+
title: "Extract an article",
|
|
40
|
+
description: "Fetch a news/blog article URL and extract the main content (headline, author, body, date) stripped of nav/ads/footer.",
|
|
41
|
+
argsSchema: {
|
|
42
|
+
url: z.string().describe("Article URL"),
|
|
43
|
+
},
|
|
44
|
+
}, ({ url }) => ({
|
|
45
|
+
messages: [
|
|
46
|
+
{
|
|
47
|
+
role: "user",
|
|
48
|
+
content: {
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `Fetch ${url} using the foura_single tool with unblocker:true. Most news and blog sites are server-rendered, so HTTP is fastest (200ms-2s).\n\n` +
|
|
51
|
+
`If foura_single returns a 403, captcha page, or empty content, retry the same URL with foura_proxy (maxTries:3) — it routes through a rotating proxy pool.\n\n` +
|
|
52
|
+
`From the response, extract:\n` +
|
|
53
|
+
`- headline (the main H1, not the page title bar)\n` +
|
|
54
|
+
`- author byline (may be inside .author / [rel=author] / itemprop)\n` +
|
|
55
|
+
`- publication date (look for <time>, .published, or JSON-LD)\n` +
|
|
56
|
+
`- main article body (strip navigation, ads, related-content, footer, comments)\n` +
|
|
57
|
+
`- canonical URL (rel=canonical or og:url)\n\n` +
|
|
58
|
+
`Return as JSON:\n` +
|
|
59
|
+
`{"title": "...", "author": "...", "date_published": "ISO8601", "body": "...", "canonical_url": "..."}`,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
}));
|
|
64
|
+
server.registerPrompt("monitor_pricing", {
|
|
65
|
+
title: "Monitor a price",
|
|
66
|
+
description: "Fetch a pricing page and extract the current price; compare to a target if provided.",
|
|
67
|
+
argsSchema: {
|
|
68
|
+
url: z.string().describe("Pricing or product page URL"),
|
|
69
|
+
target_price: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Optional target price to compare against (e.g. \"19.99\")"),
|
|
73
|
+
},
|
|
74
|
+
}, ({ url, target_price }) => ({
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: "user",
|
|
78
|
+
content: {
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `Use the foura_proxy tool with maxTries:5 and unblocker:true to fetch ${url}. Pricing pages often have aggressive bot detection, so go through the proxy pool from the start.\n\n` +
|
|
81
|
+
`Extract the current price (look for visible $/€/£ amounts, JSON-LD Offer schema, [itemprop=price]).\n\n` +
|
|
82
|
+
(target_price
|
|
83
|
+
? `Compare against target price ${target_price}: report whether current is below/at/above target, and the absolute difference.\n\n`
|
|
84
|
+
: "") +
|
|
85
|
+
`Return as JSON:\n` +
|
|
86
|
+
`{"url": "...", "current_price": 0.00, "currency": "USD"` +
|
|
87
|
+
(target_price
|
|
88
|
+
? `, "target_price": ${target_price}, "difference": 0, "status": "below|at|above"`
|
|
89
|
+
: "") +
|
|
90
|
+
`}`,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}));
|
|
95
|
+
server.registerPrompt("check_endpoint_health", {
|
|
96
|
+
title: "Check API endpoint health",
|
|
97
|
+
description: "GET a URL through foura_single with strict validation and report whether it's reachable and responding correctly.",
|
|
98
|
+
argsSchema: {
|
|
99
|
+
url: z.string().describe("HTTP endpoint URL to probe"),
|
|
100
|
+
expected_text: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Optional substring that must appear in the response body for the endpoint to count as healthy"),
|
|
104
|
+
},
|
|
105
|
+
}, ({ url, expected_text }) => ({
|
|
106
|
+
messages: [
|
|
107
|
+
{
|
|
108
|
+
role: "user",
|
|
109
|
+
content: {
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `Use the foura_single tool with GET on ${url}, timeout_ms:5000, and validate.status.accept:[200].` +
|
|
112
|
+
(expected_text
|
|
113
|
+
? ` Also set validate.data.accept:["${expected_text}"] so the request only counts as success when the body contains "${expected_text}".`
|
|
114
|
+
: "") +
|
|
115
|
+
`\n\nReport:\n` +
|
|
116
|
+
`- reachable (true if a response came back at all, false on connection error/timeout)\n` +
|
|
117
|
+
`- status_code (HTTP code from target)\n` +
|
|
118
|
+
`- total_time_ms (from the total_time field)\n` +
|
|
119
|
+
`- validation_passed (true if status + body validation conditions were met)\n\n` +
|
|
120
|
+
`Return as JSON:\n` +
|
|
121
|
+
`{"url": "...", "reachable": true, "status_code": 200, "total_time_ms": 0, "validation_passed": true}`,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
}));
|
|
126
|
+
server.registerPrompt("bulk_fetch_urls", {
|
|
127
|
+
title: "Fetch a list of URLs in parallel",
|
|
128
|
+
description: "Fetch multiple URLs concurrently and return per-URL outcome. Auto-falls back to foura_proxy on bot-block.",
|
|
129
|
+
argsSchema: {
|
|
130
|
+
urls: z.string().describe("Comma-separated list of URLs to fetch"),
|
|
131
|
+
},
|
|
132
|
+
}, ({ urls }) => ({
|
|
133
|
+
messages: [
|
|
134
|
+
{
|
|
135
|
+
role: "user",
|
|
136
|
+
content: {
|
|
137
|
+
type: "text",
|
|
138
|
+
text: `Parse the following comma-separated URLs and fetch each one concurrently using foura_single (unblocker:true).\n\n` +
|
|
139
|
+
`URLs: ${urls}\n\n` +
|
|
140
|
+
`For any URL that returns 403, captcha page, or empty body — retry that single URL with foura_proxy (maxTries:3).\n\n` +
|
|
141
|
+
`Return a JSON array, one entry per URL in input order:\n` +
|
|
142
|
+
`[{"url": "...", "status": 200, "success": true, "body_size_bytes": 0, "via": "single|proxy", "error": null}, ...]\n\n` +
|
|
143
|
+
`Do NOT inline full response bodies in the output — only metadata. If the caller needs body content, they should call foura_single individually.`,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resources — offload large response bodies (>= THRESHOLD bytes) onto host
|
|
4
|
+
* disk and return a MCP resource_link instead of inlining megabytes into the
|
|
5
|
+
* LLM context.
|
|
6
|
+
*
|
|
7
|
+
* Transport-level cross-cutting concern (same carve-out as auth.ts and
|
|
8
|
+
* safe-target.ts). Per-tool product code stays per-file: each tool decides
|
|
9
|
+
* which response field is the "large payload" (single/proxy use `data`,
|
|
10
|
+
* browser uses `body`).
|
|
11
|
+
*
|
|
12
|
+
* Audit 1.5 — tenant isolation. Payloads are stored under
|
|
13
|
+
* `<PAYLOADS_DIR>/<keyhash>/<uuid>.{bin,meta.json}`, where `keyhash` is the
|
|
14
|
+
* first 16 hex chars of sha256(apiKey). The resource handler validates the
|
|
15
|
+
* caller's keyhash matches the storage path before serving — any other
|
|
16
|
+
* tenant gets a `resource not found` (no leaking whether the UUID exists).
|
|
17
|
+
*/
|
|
18
|
+
export declare const THRESHOLD_BYTES = 50000;
|
|
19
|
+
export declare const PAYLOADS_DIR: string;
|
|
20
|
+
export interface StoredPayload {
|
|
21
|
+
uri: string;
|
|
22
|
+
name: string;
|
|
23
|
+
mimeType: string;
|
|
24
|
+
size: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function hashApiKey(apiKey: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Write `data` to disk under the caller's tenant namespace + return a
|
|
29
|
+
* resource_link descriptor. Caller has already decided the payload is large
|
|
30
|
+
* enough to offload (use THRESHOLD_BYTES for the size check).
|
|
31
|
+
*/
|
|
32
|
+
export declare function storePayload(data: Buffer | string, mimeType: string, suggestedName: string): Promise<StoredPayload>;
|
|
33
|
+
export declare function registerResourceHandler(server: McpServer): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { getApiKey } from "./auth.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resources — offload large response bodies (>= THRESHOLD bytes) onto host
|
|
9
|
+
* disk and return a MCP resource_link instead of inlining megabytes into the
|
|
10
|
+
* LLM context.
|
|
11
|
+
*
|
|
12
|
+
* Transport-level cross-cutting concern (same carve-out as auth.ts and
|
|
13
|
+
* safe-target.ts). Per-tool product code stays per-file: each tool decides
|
|
14
|
+
* which response field is the "large payload" (single/proxy use `data`,
|
|
15
|
+
* browser uses `body`).
|
|
16
|
+
*
|
|
17
|
+
* Audit 1.5 — tenant isolation. Payloads are stored under
|
|
18
|
+
* `<PAYLOADS_DIR>/<keyhash>/<uuid>.{bin,meta.json}`, where `keyhash` is the
|
|
19
|
+
* first 16 hex chars of sha256(apiKey). The resource handler validates the
|
|
20
|
+
* caller's keyhash matches the storage path before serving — any other
|
|
21
|
+
* tenant gets a `resource not found` (no leaking whether the UUID exists).
|
|
22
|
+
*/
|
|
23
|
+
export const THRESHOLD_BYTES = 50_000;
|
|
24
|
+
export const PAYLOADS_DIR = process.env.FOURA_MCP_PAYLOADS_DIR ?? path.join(tmpdir(), "foura-mcp-payloads");
|
|
25
|
+
const URI_PREFIX = "foura-mcp://payload/";
|
|
26
|
+
// sha256(apiKey).hex().slice(0, 16) — gives 64 bits of namespacing entropy,
|
|
27
|
+
// short enough for a filesystem path component and unguessable for an
|
|
28
|
+
// attacker who doesn't have the corresponding API key.
|
|
29
|
+
export function hashApiKey(apiKey) {
|
|
30
|
+
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
31
|
+
}
|
|
32
|
+
const SAFE_UUID = /^[0-9a-f-]{36}$/i;
|
|
33
|
+
const SAFE_KEYHASH = /^[0-9a-f]{16}$/;
|
|
34
|
+
const tenantDirReady = new Set();
|
|
35
|
+
async function ensureTenantDir(keyhash) {
|
|
36
|
+
const dir = path.join(PAYLOADS_DIR, keyhash);
|
|
37
|
+
if (!tenantDirReady.has(keyhash)) {
|
|
38
|
+
await mkdir(dir, { recursive: true });
|
|
39
|
+
tenantDirReady.add(keyhash);
|
|
40
|
+
}
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write `data` to disk under the caller's tenant namespace + return a
|
|
45
|
+
* resource_link descriptor. Caller has already decided the payload is large
|
|
46
|
+
* enough to offload (use THRESHOLD_BYTES for the size check).
|
|
47
|
+
*/
|
|
48
|
+
export async function storePayload(data, mimeType, suggestedName) {
|
|
49
|
+
const keyhash = hashApiKey(getApiKey());
|
|
50
|
+
const dir = await ensureTenantDir(keyhash);
|
|
51
|
+
const uuid = randomUUID();
|
|
52
|
+
const isBinary = Buffer.isBuffer(data);
|
|
53
|
+
const buf = isBinary ? data : Buffer.from(data, "utf8");
|
|
54
|
+
const dataPath = path.join(dir, `${uuid}.bin`);
|
|
55
|
+
const metaPath = path.join(dir, `${uuid}.meta.json`);
|
|
56
|
+
const meta = {
|
|
57
|
+
mimeType,
|
|
58
|
+
originalName: suggestedName,
|
|
59
|
+
size: buf.byteLength,
|
|
60
|
+
storedAt: new Date().toISOString(),
|
|
61
|
+
binary: isBinary,
|
|
62
|
+
keyhash,
|
|
63
|
+
};
|
|
64
|
+
await Promise.all([
|
|
65
|
+
writeFile(dataPath, buf),
|
|
66
|
+
writeFile(metaPath, JSON.stringify(meta), { encoding: "utf8" }),
|
|
67
|
+
]);
|
|
68
|
+
return {
|
|
69
|
+
uri: `${URI_PREFIX}${uuid}`,
|
|
70
|
+
name: suggestedName,
|
|
71
|
+
mimeType,
|
|
72
|
+
size: buf.byteLength,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function registerResourceHandler(server) {
|
|
76
|
+
server.registerResource("payload", new ResourceTemplate(`${URI_PREFIX}{uuid}`, { list: undefined }), {
|
|
77
|
+
title: "Cached foura-mcp response payload",
|
|
78
|
+
description: "A large response body (>=50KB) returned by an earlier foura-mcp tool call, " +
|
|
79
|
+
"stored on disk for follow-up reads instead of inlined into context. " +
|
|
80
|
+
"Tenant-isolated: only the API key that stored the payload can read it back.",
|
|
81
|
+
}, async (uri, { uuid }) => {
|
|
82
|
+
const uuidStr = Array.isArray(uuid) ? uuid[0] : uuid;
|
|
83
|
+
if (!uuidStr || !SAFE_UUID.test(uuidStr)) {
|
|
84
|
+
throw new Error(`Payload not found: ${uuidStr}`);
|
|
85
|
+
}
|
|
86
|
+
// Resolve the caller's tenant namespace and read ONLY from there.
|
|
87
|
+
// Cross-tenant reads bubble up as plain ENOENT (don't leak existence).
|
|
88
|
+
const keyhash = hashApiKey(getApiKey());
|
|
89
|
+
if (!SAFE_KEYHASH.test(keyhash)) {
|
|
90
|
+
throw new Error("Payload not found");
|
|
91
|
+
}
|
|
92
|
+
const dir = path.join(PAYLOADS_DIR, keyhash);
|
|
93
|
+
const metaPath = path.join(dir, `${uuidStr}.meta.json`);
|
|
94
|
+
const dataPath = path.join(dir, `${uuidStr}.bin`);
|
|
95
|
+
let metaRaw;
|
|
96
|
+
try {
|
|
97
|
+
metaRaw = await readFile(metaPath, "utf8");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new Error(`Payload not found: ${uuidStr}`);
|
|
101
|
+
}
|
|
102
|
+
const meta = JSON.parse(metaRaw);
|
|
103
|
+
// Defense in depth — even if filesystem permissions ever got bypassed,
|
|
104
|
+
// the meta sidecar carries the keyhash; reject on mismatch.
|
|
105
|
+
if (meta.keyhash !== keyhash) {
|
|
106
|
+
throw new Error(`Payload not found: ${uuidStr}`);
|
|
107
|
+
}
|
|
108
|
+
const buf = await readFile(dataPath);
|
|
109
|
+
return {
|
|
110
|
+
contents: [
|
|
111
|
+
meta.binary
|
|
112
|
+
? {
|
|
113
|
+
uri: uri.href,
|
|
114
|
+
mimeType: meta.mimeType,
|
|
115
|
+
blob: buf.toString("base64"),
|
|
116
|
+
}
|
|
117
|
+
: {
|
|
118
|
+
uri: uri.href,
|
|
119
|
+
mimeType: meta.mimeType,
|
|
120
|
+
text: buf.toString("utf8"),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIPv4, isIPv6 } from "node:net";
|
|
3
|
+
function ipv4ToInt(addr) {
|
|
4
|
+
const parts = addr.split(".");
|
|
5
|
+
if (parts.length !== 4)
|
|
6
|
+
throw new Error(`bad IPv4: ${addr}`);
|
|
7
|
+
let n = 0;
|
|
8
|
+
for (const p of parts) {
|
|
9
|
+
const v = Number(p);
|
|
10
|
+
if (!Number.isInteger(v) || v < 0 || v > 255)
|
|
11
|
+
throw new Error(`bad IPv4 octet: ${addr}`);
|
|
12
|
+
n = n * 256 + v;
|
|
13
|
+
}
|
|
14
|
+
return n >>> 0;
|
|
15
|
+
}
|
|
16
|
+
// RFC 5735 + RFC 6598 (CGNAT) reserved IPv4 blocks.
|
|
17
|
+
const V4_RESERVED = [
|
|
18
|
+
{ base: ipv4ToInt("0.0.0.0"), prefix: 8 }, // "this network"
|
|
19
|
+
{ base: ipv4ToInt("10.0.0.0"), prefix: 8 }, // RFC1918 private
|
|
20
|
+
{ base: ipv4ToInt("100.64.0.0"), prefix: 10 }, // CGNAT (RFC 6598)
|
|
21
|
+
{ base: ipv4ToInt("127.0.0.0"), prefix: 8 }, // loopback
|
|
22
|
+
{ base: ipv4ToInt("169.254.0.0"), prefix: 16 }, // link-local
|
|
23
|
+
{ base: ipv4ToInt("172.16.0.0"), prefix: 12 }, // RFC1918 private
|
|
24
|
+
{ base: ipv4ToInt("192.0.0.0"), prefix: 24 }, // IETF protocol
|
|
25
|
+
{ base: ipv4ToInt("192.0.2.0"), prefix: 24 }, // TEST-NET-1
|
|
26
|
+
{ base: ipv4ToInt("192.88.99.0"), prefix: 24 }, // 6to4 anycast (deprecated)
|
|
27
|
+
{ base: ipv4ToInt("192.168.0.0"), prefix: 16 }, // RFC1918 private
|
|
28
|
+
{ base: ipv4ToInt("198.18.0.0"), prefix: 15 }, // benchmarking
|
|
29
|
+
{ base: ipv4ToInt("198.51.100.0"), prefix: 24 }, // TEST-NET-2
|
|
30
|
+
{ base: ipv4ToInt("203.0.113.0"), prefix: 24 }, // TEST-NET-3
|
|
31
|
+
{ base: ipv4ToInt("224.0.0.0"), prefix: 4 }, // multicast
|
|
32
|
+
{ base: ipv4ToInt("240.0.0.0"), prefix: 4 }, // reserved
|
|
33
|
+
{ base: ipv4ToInt("255.255.255.255"), prefix: 32 }, // broadcast
|
|
34
|
+
];
|
|
35
|
+
function isReservedV4(addr) {
|
|
36
|
+
let n;
|
|
37
|
+
try {
|
|
38
|
+
n = ipv4ToInt(addr);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
for (const b of V4_RESERVED) {
|
|
44
|
+
const mask = b.prefix === 0 ? 0 : (0xffffffff << (32 - b.prefix)) >>> 0;
|
|
45
|
+
if ((n & mask) === (b.base & mask))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function isReservedV6(addr) {
|
|
51
|
+
const a = addr.toLowerCase();
|
|
52
|
+
if (a === "::" || a === "::1")
|
|
53
|
+
return true;
|
|
54
|
+
const firstGroup = a.split(":")[0] ?? "";
|
|
55
|
+
// ULA fc00::/7 — first hex of first group is f, second is c or d
|
|
56
|
+
if (/^f[cd][0-9a-f]{0,2}$/.test(firstGroup))
|
|
57
|
+
return true;
|
|
58
|
+
// link-local fe80::/10 — first group starts fe8, fe9, fea, feb
|
|
59
|
+
if (/^fe[89ab][0-9a-f]{0,1}$/.test(firstGroup))
|
|
60
|
+
return true;
|
|
61
|
+
// documentation 2001:db8::/32
|
|
62
|
+
if (/^2001:0?db8(:|$)/.test(a))
|
|
63
|
+
return true;
|
|
64
|
+
// IPv4-mapped: ::ffff:x.x.x.x (dotted-quad form) — check embedded v4
|
|
65
|
+
const v4mappedDotted = /^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/.exec(a);
|
|
66
|
+
if (v4mappedDotted?.[1])
|
|
67
|
+
return isReservedV4(v4mappedDotted[1]);
|
|
68
|
+
// IPv4-mapped canonical hex form (Node URL normalises ::ffff:192.168.1.1
|
|
69
|
+
// to ::ffff:c0a8:101). Match ::ffff: prefix + two hex groups, decode.
|
|
70
|
+
const v4mappedHex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(a);
|
|
71
|
+
if (v4mappedHex) {
|
|
72
|
+
const high = parseInt(v4mappedHex[1] ?? "0", 16);
|
|
73
|
+
const low = parseInt(v4mappedHex[2] ?? "0", 16);
|
|
74
|
+
const o1 = (high >> 8) & 0xff;
|
|
75
|
+
const o2 = high & 0xff;
|
|
76
|
+
const o3 = (low >> 8) & 0xff;
|
|
77
|
+
const o4 = low & 0xff;
|
|
78
|
+
return isReservedV4(`${o1}.${o2}.${o3}.${o4}`);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
export class SsrfBlockedError extends Error {
|
|
83
|
+
hostInfo;
|
|
84
|
+
constructor(hostInfo) {
|
|
85
|
+
super(`Refusing to fetch ${hostInfo}: target resolves to a private or reserved IP range. ` +
|
|
86
|
+
`The FourA scraping API only forwards requests to public internet hosts.`);
|
|
87
|
+
this.hostInfo = hostInfo;
|
|
88
|
+
this.name = "SsrfBlockedError";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function assertPublicTarget(rawUrl) {
|
|
92
|
+
let url;
|
|
93
|
+
try {
|
|
94
|
+
url = new URL(rawUrl);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new SsrfBlockedError(`invalid URL: ${rawUrl}`);
|
|
98
|
+
}
|
|
99
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
100
|
+
throw new SsrfBlockedError(`unsupported scheme ${url.protocol}`);
|
|
101
|
+
}
|
|
102
|
+
// Strip the [...] brackets from a bare IPv6 hostname
|
|
103
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
104
|
+
if (isIPv4(host)) {
|
|
105
|
+
if (isReservedV4(host))
|
|
106
|
+
throw new SsrfBlockedError(host);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (isIPv6(host)) {
|
|
110
|
+
if (isReservedV6(host))
|
|
111
|
+
throw new SsrfBlockedError(host);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let addrs;
|
|
115
|
+
try {
|
|
116
|
+
addrs = await lookup(host, { all: true, verbatim: true });
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
throw new SsrfBlockedError(`DNS lookup failed for ${host}: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
if (addrs.length === 0) {
|
|
122
|
+
throw new SsrfBlockedError(`${host} resolved to no addresses`);
|
|
123
|
+
}
|
|
124
|
+
for (const a of addrs) {
|
|
125
|
+
const bad = a.family === 4 ? isReservedV4(a.address) : isReservedV6(a.address);
|
|
126
|
+
if (bad)
|
|
127
|
+
throw new SsrfBlockedError(`${host} → ${a.address}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=safe-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-target.js","sourceRoot":"","sources":["../src/safe-target.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerSingleTool } from "./tools/single.js";
|
|
3
|
+
import { registerProxyTool } from "./tools/proxy.js";
|
|
4
|
+
import { registerBrowserTool } from "./tools/browser.js";
|
|
5
|
+
import { registerResourceHandler } from "./resources.js";
|
|
6
|
+
import { registerPrompts } from "./prompts.js";
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "foura-mcp",
|
|
10
|
+
version: "0.2.11",
|
|
11
|
+
});
|
|
12
|
+
registerSingleTool(server);
|
|
13
|
+
registerProxyTool(server);
|
|
14
|
+
registerBrowserTool(server);
|
|
15
|
+
registerResourceHandler(server);
|
|
16
|
+
registerPrompts(server);
|
|
17
|
+
return server;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,QAAQ;KAClB,CAAC,CAAC;IAEH,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAChC,eAAe,CAAC,MAAM,CAAC,CAAC;IAExB,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/stdio.d.ts
ADDED
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;AAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
declare function deriveCode(status: number, envelope: Record<string, unknown>): string;
|
|
4
|
+
export declare function registerBrowserTool(server: McpServer): void;
|
|
5
|
+
export declare const __test: {
|
|
6
|
+
deriveCode: typeof deriveCode;
|
|
7
|
+
CdpCookieSchema: z.ZodObject<{
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
value: z.ZodString;
|
|
10
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
11
|
+
path: z.ZodOptional<z.ZodString>;
|
|
12
|
+
expires: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
httpOnly: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
secure: z.ZodOptional<z.ZodBoolean>;
|
|
16
|
+
session: z.ZodOptional<z.ZodBoolean>;
|
|
17
|
+
sameSite: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$catchall<z.ZodUnknown>>;
|
|
19
|
+
BrowserCookieInputSchema: z.ZodObject<{
|
|
20
|
+
name: z.ZodString;
|
|
21
|
+
value: z.ZodString;
|
|
22
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
browserInputShape: {
|
|
25
|
+
url: z.ZodString;
|
|
26
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
27
|
+
cookies: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
value: z.ZodString;
|
|
30
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>>>;
|
|
32
|
+
userAgent: z.ZodOptional<z.ZodString>;
|
|
33
|
+
proxy: z.ZodOptional<z.ZodString>;
|
|
34
|
+
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
checkStatus: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
checkText: z.ZodOptional<z.ZodString>;
|
|
37
|
+
offload_large: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
};
|
|
39
|
+
browserOutputShape: {
|
|
40
|
+
status: z.ZodOptional<z.ZodNumber>;
|
|
41
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
42
|
+
body: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
|
|
43
|
+
cookies: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
44
|
+
name: z.ZodString;
|
|
45
|
+
value: z.ZodString;
|
|
46
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
47
|
+
path: z.ZodOptional<z.ZodString>;
|
|
48
|
+
expires: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
50
|
+
httpOnly: z.ZodOptional<z.ZodBoolean>;
|
|
51
|
+
secure: z.ZodOptional<z.ZodBoolean>;
|
|
52
|
+
session: z.ZodOptional<z.ZodBoolean>;
|
|
53
|
+
sameSite: z.ZodOptional<z.ZodString>;
|
|
54
|
+
}, z.core.$catchall<z.ZodUnknown>>>>;
|
|
55
|
+
userAgent: z.ZodOptional<z.ZodString>;
|
|
56
|
+
offloaded_resource_uri: z.ZodOptional<z.ZodString>;
|
|
57
|
+
size_bytes: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
error: z.ZodOptional<z.ZodString>;
|
|
59
|
+
service: z.ZodOptional<z.ZodEnum<{
|
|
60
|
+
single: "single";
|
|
61
|
+
proxy: "proxy";
|
|
62
|
+
browser: "browser";
|
|
63
|
+
api: "api";
|
|
64
|
+
}>>;
|
|
65
|
+
retryAfter: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
current: z.ZodOptional<z.ZodObject<{
|
|
67
|
+
concurrency: z.ZodOptional<z.ZodNumber>;
|
|
68
|
+
rpm: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
}, z.core.$strip>>;
|
|
70
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
71
|
+
maxConcurrency: z.ZodOptional<z.ZodNumber>;
|
|
72
|
+
maxRpm: z.ZodOptional<z.ZodNumber>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
code: z.ZodOptional<z.ZodString>;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export {};
|