@jsonrecon/mcp-server 1.2.0 → 1.3.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 +58 -0
- package/dist/index.js +183 -53
- package/package.json +20 -16
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# JSON Recon MCP Server
|
|
2
|
+
|
|
3
|
+
MCP tools for concise Schema.org extraction and field-level Product verification.
|
|
4
|
+
|
|
5
|
+
## Tools and prices
|
|
6
|
+
|
|
7
|
+
| Tool | Mode | Price on success |
|
|
8
|
+
| --- | --- | ---: |
|
|
9
|
+
| `check_extractability` | Free preflight | $0 |
|
|
10
|
+
| `extract_schema` | Lite | $0.005 |
|
|
11
|
+
| `extract_schema` | Full | $0.01 |
|
|
12
|
+
| `verify_product` | Verified Product | $0.02 |
|
|
13
|
+
|
|
14
|
+
Paid tools support either a prepaid JSON Recon API key or an agent-controlled wallet using x402 on Base. The free preflight works without credentials.
|
|
15
|
+
|
|
16
|
+
## Prepaid API-key setup
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"jsonrecon": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "@jsonrecon/mcp-server"],
|
|
24
|
+
"env": {
|
|
25
|
+
"JSONRECON_API_KEY": "jr_liv_xxxxx"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## x402 wallet setup
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"jsonrecon": {
|
|
38
|
+
"command": "npx",
|
|
39
|
+
"args": ["-y", "@jsonrecon/mcp-server"],
|
|
40
|
+
"env": {
|
|
41
|
+
"JSONRECON_PAYMENT_MODE": "x402",
|
|
42
|
+
"JSONRECON_EVM_PRIVATE_KEY": "0x_your_32_byte_private_key",
|
|
43
|
+
"JSONRECON_X402_SESSION_LIMIT_USDC": "1.00"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use a dedicated, low-balance agent wallet. The private key remains in the local MCP process and is never sent to JSON Recon; only the signed x402 payment authorization is transmitted. The client signs only exact Base-mainnet USDC challenges from the configured JSON Recon origin and route, subject to fixed per-route price ceilings. Redirects are disabled. The default cumulative authorization ceiling is $1 USDC per MCP process and can be lowered with `JSONRECON_X402_SESSION_LIMIT_USDC`.
|
|
51
|
+
|
|
52
|
+
When both credentials are configured, the default `auto` mode prefers the API key. Set `JSONRECON_PAYMENT_MODE` explicitly to `api_key` or `x402` to override that selection.
|
|
53
|
+
|
|
54
|
+
## Free-only setup
|
|
55
|
+
|
|
56
|
+
Omit the `env` block to use `check_extractability` without credentials. Paid tool calls return a configuration error until either payment method is configured.
|
|
57
|
+
|
|
58
|
+
Learn more at [jsonrecon.com/mcp](https://jsonrecon.com/mcp).
|
package/dist/index.js
CHANGED
|
@@ -3,95 +3,225 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
|
+
import { wrapAxiosWithPayment, x402Client, } from "@x402/axios";
|
|
7
|
+
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
8
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
6
9
|
import { TOOLS } from "./tools.js";
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const VERSION = "1.3.0";
|
|
11
|
+
const BASE_NETWORK = "eip155:8453";
|
|
12
|
+
const BASE_USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
13
|
+
const DEFAULT_ENDPOINT = "https://jsonrecon.com";
|
|
14
|
+
const DEFAULT_SESSION_LIMIT_USDC = "1.00";
|
|
15
|
+
const ROUTE_LIMITS = {
|
|
16
|
+
"/extract": 10000n,
|
|
17
|
+
"/extract/lite": 5000n,
|
|
18
|
+
"/extract/verified": 20000n,
|
|
19
|
+
};
|
|
20
|
+
function normalizeEndpoint(rawEndpoint) {
|
|
21
|
+
let endpoint;
|
|
22
|
+
try {
|
|
23
|
+
endpoint = new URL(rawEndpoint);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error("JSONRECON_ENDPOINT must be a valid URL.");
|
|
27
|
+
}
|
|
28
|
+
const isLoopback = ["127.0.0.1", "::1", "localhost"].includes(endpoint.hostname);
|
|
29
|
+
if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && isLoopback)) {
|
|
30
|
+
throw new Error("JSONRECON_ENDPOINT must use HTTPS (HTTP is allowed only for loopback testing).");
|
|
31
|
+
}
|
|
32
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
33
|
+
throw new Error("JSONRECON_ENDPOINT must not contain credentials, a query, or a fragment.");
|
|
34
|
+
}
|
|
35
|
+
if (endpoint.pathname !== "/" && endpoint.pathname !== "") {
|
|
36
|
+
throw new Error("JSONRECON_ENDPOINT must be an origin without a path.");
|
|
37
|
+
}
|
|
38
|
+
endpoint.pathname = "/";
|
|
39
|
+
return endpoint;
|
|
15
40
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
function parseUsdc(value, variableName) {
|
|
42
|
+
if (!/^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/.test(value)) {
|
|
43
|
+
throw new Error(`${variableName} must be a non-negative USDC amount with at most 6 decimals.`);
|
|
44
|
+
}
|
|
45
|
+
const [whole, fraction = ""] = value.split(".");
|
|
46
|
+
return BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
|
|
47
|
+
}
|
|
48
|
+
function configuredMode() {
|
|
49
|
+
const value = (process.env.JSONRECON_PAYMENT_MODE || "auto").toLowerCase();
|
|
50
|
+
if (value !== "auto" && value !== "api_key" && value !== "x402") {
|
|
51
|
+
throw new Error("JSONRECON_PAYMENT_MODE must be auto, api_key, or x402.");
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function paidAccessMode() {
|
|
56
|
+
const mode = configuredMode();
|
|
57
|
+
if (mode === "api_key") {
|
|
58
|
+
if (!process.env.JSONRECON_API_KEY) {
|
|
59
|
+
throw new Error("API-key mode requires JSONRECON_API_KEY.");
|
|
60
|
+
}
|
|
61
|
+
return mode;
|
|
62
|
+
}
|
|
63
|
+
if (mode === "x402") {
|
|
64
|
+
if (!process.env.JSONRECON_EVM_PRIVATE_KEY) {
|
|
65
|
+
throw new Error("x402 mode requires JSONRECON_EVM_PRIVATE_KEY.");
|
|
66
|
+
}
|
|
67
|
+
return mode;
|
|
68
|
+
}
|
|
69
|
+
if (process.env.JSONRECON_API_KEY)
|
|
70
|
+
return "api_key";
|
|
71
|
+
if (process.env.JSONRECON_EVM_PRIVATE_KEY)
|
|
72
|
+
return "x402";
|
|
73
|
+
throw new Error("Paid tools require JSONRECON_API_KEY, or JSONRECON_PAYMENT_MODE=x402 with JSONRECON_EVM_PRIVATE_KEY.");
|
|
74
|
+
}
|
|
75
|
+
const endpoint = normalizeEndpoint(process.env.JSONRECON_ENDPOINT || DEFAULT_ENDPOINT);
|
|
76
|
+
const endpointOrigin = endpoint.origin;
|
|
77
|
+
const plainApi = axios.create({
|
|
78
|
+
baseURL: endpointOrigin,
|
|
79
|
+
maxRedirects: 0,
|
|
80
|
+
timeout: 120_000,
|
|
27
81
|
});
|
|
28
|
-
|
|
82
|
+
let authorizedThisSession = 0n;
|
|
83
|
+
function getSessionLimit() {
|
|
84
|
+
return parseUsdc(process.env.JSONRECON_X402_SESSION_LIMIT_USDC || DEFAULT_SESSION_LIMIT_USDC, "JSONRECON_X402_SESSION_LIMIT_USDC");
|
|
85
|
+
}
|
|
86
|
+
function createX402Api(route) {
|
|
87
|
+
const privateKey = process.env.JSONRECON_EVM_PRIVATE_KEY;
|
|
88
|
+
if (!privateKey || !/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
|
|
89
|
+
throw new Error("JSONRECON_EVM_PRIVATE_KEY must be a 0x-prefixed 32-byte EVM private key.");
|
|
90
|
+
}
|
|
91
|
+
const account = privateKeyToAccount(privateKey);
|
|
92
|
+
const routeLimit = ROUTE_LIMITS[route];
|
|
93
|
+
const client = new x402Client()
|
|
94
|
+
.register(BASE_NETWORK, new ExactEvmScheme(account))
|
|
95
|
+
.registerPolicy((version, requirements) => requirements.filter((requirement) => {
|
|
96
|
+
if (version !== 2
|
|
97
|
+
|| requirement.scheme !== "exact"
|
|
98
|
+
|| requirement.network !== BASE_NETWORK
|
|
99
|
+
|| requirement.asset.toLowerCase() !== BASE_USDC
|
|
100
|
+
|| !Number.isSafeInteger(requirement.maxTimeoutSeconds)
|
|
101
|
+
|| requirement.maxTimeoutSeconds <= 0
|
|
102
|
+
|| requirement.maxTimeoutSeconds > 300) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const amount = BigInt(requirement.amount);
|
|
107
|
+
return amount > 0n && amount <= routeLimit;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
client.onBeforePaymentCreation(async ({ paymentRequired, selectedRequirements }) => {
|
|
114
|
+
let resource;
|
|
115
|
+
try {
|
|
116
|
+
resource = new URL(paymentRequired.resource.url);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { abort: true, reason: "The payment challenge has an invalid resource URL." };
|
|
120
|
+
}
|
|
121
|
+
if (resource.origin !== endpointOrigin || resource.pathname !== route) {
|
|
122
|
+
return { abort: true, reason: "The payment challenge does not match the requested JSON Recon route." };
|
|
123
|
+
}
|
|
124
|
+
const amount = BigInt(selectedRequirements.amount);
|
|
125
|
+
const sessionLimit = getSessionLimit();
|
|
126
|
+
if (authorizedThisSession + amount > sessionLimit) {
|
|
127
|
+
return { abort: true, reason: "The JSON Recon x402 session spending limit would be exceeded." };
|
|
128
|
+
}
|
|
129
|
+
authorizedThisSession += amount;
|
|
130
|
+
});
|
|
131
|
+
return wrapAxiosWithPayment(axios.create({
|
|
132
|
+
baseURL: endpointOrigin,
|
|
133
|
+
maxRedirects: 0,
|
|
134
|
+
timeout: 120_000,
|
|
135
|
+
}), client);
|
|
136
|
+
}
|
|
137
|
+
async function postPaid(route, body) {
|
|
138
|
+
const mode = paidAccessMode();
|
|
139
|
+
if (mode === "api_key") {
|
|
140
|
+
return plainApi.post(route, body, {
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${process.env.JSONRECON_API_KEY}`,
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return createX402Api(route).post(route, body, {
|
|
148
|
+
headers: { "Content-Type": "application/json" },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function safeError(error) {
|
|
152
|
+
if (axios.isAxiosError(error)) {
|
|
153
|
+
const body = error.response?.data;
|
|
154
|
+
return {
|
|
155
|
+
error: typeof body?.error === "string"
|
|
156
|
+
? body.error
|
|
157
|
+
: `JSON Recon request failed${error.response?.status ? ` with HTTP ${error.response.status}` : ""}.`,
|
|
158
|
+
...(typeof body?.reason === "string" ? { reason: body.reason } : {}),
|
|
159
|
+
...(typeof body?.refunded === "boolean" ? { refunded: body.refunded } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const message = error instanceof Error ? error.message : "Unknown request failure.";
|
|
163
|
+
if (message.startsWith("Failed to create payment payload:")) {
|
|
164
|
+
return { error: "The x402 payment challenge was rejected or could not be signed safely." };
|
|
165
|
+
}
|
|
166
|
+
const allowed = [
|
|
167
|
+
"API-key mode requires",
|
|
168
|
+
"x402 mode requires",
|
|
169
|
+
"Paid tools require",
|
|
170
|
+
"JSONRECON_PAYMENT_MODE",
|
|
171
|
+
"JSONRECON_EVM_PRIVATE_KEY",
|
|
172
|
+
"JSONRECON_X402_SESSION_LIMIT_USDC",
|
|
173
|
+
].some(prefix => message.startsWith(prefix));
|
|
174
|
+
return { error: allowed ? message : "JSON Recon request failed before a safe response was received." };
|
|
175
|
+
}
|
|
176
|
+
const server = new Server({ name: "jsonrecon-mcp", version: VERSION }, { capabilities: { tools: {} } });
|
|
177
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
29
178
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
30
179
|
if (request.params.name === "extract_schema") {
|
|
31
180
|
const url = String(request.params.arguments?.url);
|
|
32
181
|
const schemaType = String(request.params.arguments?.schema_type || "auto");
|
|
33
182
|
const mode = String(request.params.arguments?.mode || "full");
|
|
183
|
+
const route = mode === "lite" ? "/extract/lite" : "/extract";
|
|
34
184
|
try {
|
|
35
|
-
const
|
|
36
|
-
const response = await axios.post(`${ENDPOINT}${endpointPath}`, {
|
|
37
|
-
url: url,
|
|
38
|
-
schema_type: schemaType,
|
|
39
|
-
}, {
|
|
40
|
-
headers: {
|
|
41
|
-
"Authorization": `Bearer ${API_KEY}`,
|
|
42
|
-
"Content-Type": "application/json"
|
|
43
|
-
},
|
|
44
|
-
});
|
|
185
|
+
const response = await postPaid(route, { url, schema_type: schemaType });
|
|
45
186
|
return {
|
|
46
187
|
structuredContent: response.data,
|
|
47
|
-
content: [
|
|
48
|
-
{
|
|
49
|
-
type: "text",
|
|
50
|
-
text: JSON.stringify(response.data, null, 2),
|
|
51
|
-
},
|
|
52
|
-
],
|
|
188
|
+
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
53
189
|
};
|
|
54
190
|
}
|
|
55
191
|
catch (error) {
|
|
56
|
-
const errorMsg = error.response?.data?.error || error.message;
|
|
57
192
|
return {
|
|
58
193
|
isError: true,
|
|
59
|
-
content: [
|
|
60
|
-
{
|
|
61
|
-
type: "text",
|
|
62
|
-
text: `JsonRecon API Error: ${errorMsg}`,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify(safeError(error), null, 2) }],
|
|
65
195
|
};
|
|
66
196
|
}
|
|
67
197
|
}
|
|
68
198
|
if (request.params.name === "verify_product") {
|
|
69
199
|
const url = String(request.params.arguments?.url);
|
|
70
200
|
try {
|
|
71
|
-
const response = await
|
|
201
|
+
const response = await postPaid("/extract/verified", { url });
|
|
72
202
|
return {
|
|
73
203
|
structuredContent: response.data,
|
|
74
204
|
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
|
|
75
205
|
};
|
|
76
206
|
}
|
|
77
207
|
catch (error) {
|
|
78
|
-
const body = error.response?.data;
|
|
79
|
-
const errorMsg = body?.error || error.message;
|
|
80
208
|
return {
|
|
81
209
|
isError: true,
|
|
82
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
210
|
+
content: [{ type: "text", text: JSON.stringify(safeError(error), null, 2) }],
|
|
83
211
|
};
|
|
84
212
|
}
|
|
85
213
|
}
|
|
86
214
|
if (request.params.name === "check_extractability") {
|
|
87
215
|
const url = String(request.params.arguments?.url);
|
|
88
216
|
try {
|
|
89
|
-
const response = await
|
|
217
|
+
const response = await plainApi.get("/check", { params: { url } });
|
|
90
218
|
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
|
|
91
219
|
}
|
|
92
220
|
catch (error) {
|
|
93
|
-
|
|
94
|
-
|
|
221
|
+
return {
|
|
222
|
+
isError: true,
|
|
223
|
+
content: [{ type: "text", text: JSON.stringify(safeError(error), null, 2) }],
|
|
224
|
+
};
|
|
95
225
|
}
|
|
96
226
|
}
|
|
97
227
|
throw new Error(`Tool not found: ${request.params.name}`);
|
|
@@ -99,9 +229,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
99
229
|
async function main() {
|
|
100
230
|
const transport = new StdioServerTransport();
|
|
101
231
|
await server.connect(transport);
|
|
102
|
-
console.error("
|
|
232
|
+
console.error("JSON Recon MCP server running on stdio");
|
|
103
233
|
}
|
|
104
|
-
main().catch((
|
|
105
|
-
console.error("
|
|
234
|
+
main().catch(() => {
|
|
235
|
+
console.error("JSON Recon MCP server could not start. Check its non-secret configuration.");
|
|
106
236
|
process.exit(1);
|
|
107
237
|
});
|
package/package.json
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsonrecon/mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP tools for concise Schema.org extraction and Product verification",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "MCP tools for concise Schema.org extraction and Product verification",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"jsonrecon-mcp": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist"
|
|
12
|
-
],
|
|
13
|
-
"publishConfig": {
|
|
14
|
-
"access": "public"
|
|
15
|
-
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"schemas:check": "node ../../scripts/sync-mcp-output-schemas.js --check",
|
|
18
|
-
"prebuild": "npm run schemas:check",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jsonrecon-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"schemas:check": "node ../../scripts/sync-mcp-output-schemas.js --check",
|
|
18
|
+
"prebuild": "npm run schemas:check",
|
|
19
19
|
"build": "tsc",
|
|
20
20
|
"test": "node --test test/*.test.mjs",
|
|
21
|
+
"test:live": "npm run build && node test/live-canary.mjs",
|
|
21
22
|
"prepublishOnly": "npm run build"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
25
|
-
"axios": "
|
|
26
|
+
"@x402/axios": "2.19.0",
|
|
27
|
+
"@x402/evm": "2.19.0",
|
|
28
|
+
"axios": "^1.7.0",
|
|
29
|
+
"viem": "2.55.4"
|
|
26
30
|
},
|
|
27
31
|
"devDependencies": {
|
|
28
32
|
"@types/node": "^20.0.0",
|