@jsonrecon/mcp-server 1.2.0 → 1.3.1

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.
Files changed (3) hide show
  1. package/README.md +58 -0
  2. package/dist/index.js +190 -53
  3. 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,232 @@ 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
- // Environment setup
8
- const API_KEY = process.env.JSONRECON_API_KEY;
9
- // For dev/testing, user might override the endpoint
10
- const ENDPOINT = process.env.JSONRECON_ENDPOINT || "https://jsonrecon.com";
11
- if (!API_KEY) {
12
- console.error("Missing JSONRECON_API_KEY environment variable.");
13
- console.error("Please add it to your MCP configuration.");
14
- process.exit(1);
15
- }
16
- const server = new Server({
17
- name: "jsonrecon-mcp",
18
- version: "1.2.0",
19
- }, {
20
- capabilities: {
21
- tools: {},
22
- },
10
+ const VERSION = "1.3.1";
11
+ const CLIENT_ID = `mcp-server/${VERSION}`;
12
+ const CLIENT_HEADERS = Object.freeze({
13
+ "User-Agent": `@jsonrecon/mcp-server/${VERSION}`,
14
+ "X-JSONRecon-Client": CLIENT_ID,
23
15
  });
24
- // Register tools
25
- server.setRequestHandler(ListToolsRequestSchema, async () => {
26
- return { tools: TOOLS };
16
+ const BASE_NETWORK = "eip155:8453";
17
+ const BASE_USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
18
+ const DEFAULT_ENDPOINT = "https://jsonrecon.com";
19
+ const DEFAULT_SESSION_LIMIT_USDC = "1.00";
20
+ const ROUTE_LIMITS = {
21
+ "/extract": 10000n,
22
+ "/extract/lite": 5000n,
23
+ "/extract/verified": 20000n,
24
+ };
25
+ function normalizeEndpoint(rawEndpoint) {
26
+ let endpoint;
27
+ try {
28
+ endpoint = new URL(rawEndpoint);
29
+ }
30
+ catch {
31
+ throw new Error("JSONRECON_ENDPOINT must be a valid URL.");
32
+ }
33
+ const isLoopback = ["127.0.0.1", "::1", "localhost"].includes(endpoint.hostname);
34
+ if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && isLoopback)) {
35
+ throw new Error("JSONRECON_ENDPOINT must use HTTPS (HTTP is allowed only for loopback testing).");
36
+ }
37
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
38
+ throw new Error("JSONRECON_ENDPOINT must not contain credentials, a query, or a fragment.");
39
+ }
40
+ if (endpoint.pathname !== "/" && endpoint.pathname !== "") {
41
+ throw new Error("JSONRECON_ENDPOINT must be an origin without a path.");
42
+ }
43
+ endpoint.pathname = "/";
44
+ return endpoint;
45
+ }
46
+ function parseUsdc(value, variableName) {
47
+ if (!/^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/.test(value)) {
48
+ throw new Error(`${variableName} must be a non-negative USDC amount with at most 6 decimals.`);
49
+ }
50
+ const [whole, fraction = ""] = value.split(".");
51
+ return BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
52
+ }
53
+ function configuredMode() {
54
+ const value = (process.env.JSONRECON_PAYMENT_MODE || "auto").toLowerCase();
55
+ if (value !== "auto" && value !== "api_key" && value !== "x402") {
56
+ throw new Error("JSONRECON_PAYMENT_MODE must be auto, api_key, or x402.");
57
+ }
58
+ return value;
59
+ }
60
+ function paidAccessMode() {
61
+ const mode = configuredMode();
62
+ if (mode === "api_key") {
63
+ if (!process.env.JSONRECON_API_KEY) {
64
+ throw new Error("API-key mode requires JSONRECON_API_KEY.");
65
+ }
66
+ return mode;
67
+ }
68
+ if (mode === "x402") {
69
+ if (!process.env.JSONRECON_EVM_PRIVATE_KEY) {
70
+ throw new Error("x402 mode requires JSONRECON_EVM_PRIVATE_KEY.");
71
+ }
72
+ return mode;
73
+ }
74
+ if (process.env.JSONRECON_API_KEY)
75
+ return "api_key";
76
+ if (process.env.JSONRECON_EVM_PRIVATE_KEY)
77
+ return "x402";
78
+ throw new Error("Paid tools require JSONRECON_API_KEY, or JSONRECON_PAYMENT_MODE=x402 with JSONRECON_EVM_PRIVATE_KEY.");
79
+ }
80
+ const endpoint = normalizeEndpoint(process.env.JSONRECON_ENDPOINT || DEFAULT_ENDPOINT);
81
+ const endpointOrigin = endpoint.origin;
82
+ const plainApi = axios.create({
83
+ baseURL: endpointOrigin,
84
+ headers: CLIENT_HEADERS,
85
+ maxRedirects: 0,
86
+ timeout: 120_000,
27
87
  });
28
- // Handle tool execution
88
+ let authorizedThisSession = 0n;
89
+ function getSessionLimit() {
90
+ return parseUsdc(process.env.JSONRECON_X402_SESSION_LIMIT_USDC || DEFAULT_SESSION_LIMIT_USDC, "JSONRECON_X402_SESSION_LIMIT_USDC");
91
+ }
92
+ function createX402Api(route) {
93
+ const privateKey = process.env.JSONRECON_EVM_PRIVATE_KEY;
94
+ if (!privateKey || !/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
95
+ throw new Error("JSONRECON_EVM_PRIVATE_KEY must be a 0x-prefixed 32-byte EVM private key.");
96
+ }
97
+ const account = privateKeyToAccount(privateKey);
98
+ const routeLimit = ROUTE_LIMITS[route];
99
+ const client = new x402Client()
100
+ .register(BASE_NETWORK, new ExactEvmScheme(account))
101
+ .registerPolicy((version, requirements) => requirements.filter((requirement) => {
102
+ if (version !== 2
103
+ || requirement.scheme !== "exact"
104
+ || requirement.network !== BASE_NETWORK
105
+ || requirement.asset.toLowerCase() !== BASE_USDC
106
+ || !Number.isSafeInteger(requirement.maxTimeoutSeconds)
107
+ || requirement.maxTimeoutSeconds <= 0
108
+ || requirement.maxTimeoutSeconds > 300) {
109
+ return false;
110
+ }
111
+ try {
112
+ const amount = BigInt(requirement.amount);
113
+ return amount > 0n && amount <= routeLimit;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }));
119
+ client.onBeforePaymentCreation(async ({ paymentRequired, selectedRequirements }) => {
120
+ let resource;
121
+ try {
122
+ resource = new URL(paymentRequired.resource.url);
123
+ }
124
+ catch {
125
+ return { abort: true, reason: "The payment challenge has an invalid resource URL." };
126
+ }
127
+ if (resource.origin !== endpointOrigin || resource.pathname !== route) {
128
+ return { abort: true, reason: "The payment challenge does not match the requested JSON Recon route." };
129
+ }
130
+ const amount = BigInt(selectedRequirements.amount);
131
+ const sessionLimit = getSessionLimit();
132
+ if (authorizedThisSession + amount > sessionLimit) {
133
+ return { abort: true, reason: "The JSON Recon x402 session spending limit would be exceeded." };
134
+ }
135
+ authorizedThisSession += amount;
136
+ });
137
+ return wrapAxiosWithPayment(axios.create({
138
+ baseURL: endpointOrigin,
139
+ headers: CLIENT_HEADERS,
140
+ maxRedirects: 0,
141
+ timeout: 120_000,
142
+ }), client);
143
+ }
144
+ async function postPaid(route, body) {
145
+ const mode = paidAccessMode();
146
+ if (mode === "api_key") {
147
+ return plainApi.post(route, body, {
148
+ headers: {
149
+ Authorization: `Bearer ${process.env.JSONRECON_API_KEY}`,
150
+ "Content-Type": "application/json",
151
+ },
152
+ });
153
+ }
154
+ return createX402Api(route).post(route, body, {
155
+ headers: { "Content-Type": "application/json" },
156
+ });
157
+ }
158
+ function safeError(error) {
159
+ if (axios.isAxiosError(error)) {
160
+ const body = error.response?.data;
161
+ return {
162
+ error: typeof body?.error === "string"
163
+ ? body.error
164
+ : `JSON Recon request failed${error.response?.status ? ` with HTTP ${error.response.status}` : ""}.`,
165
+ ...(typeof body?.reason === "string" ? { reason: body.reason } : {}),
166
+ ...(typeof body?.refunded === "boolean" ? { refunded: body.refunded } : {}),
167
+ };
168
+ }
169
+ const message = error instanceof Error ? error.message : "Unknown request failure.";
170
+ if (message.startsWith("Failed to create payment payload:")) {
171
+ return { error: "The x402 payment challenge was rejected or could not be signed safely." };
172
+ }
173
+ const allowed = [
174
+ "API-key mode requires",
175
+ "x402 mode requires",
176
+ "Paid tools require",
177
+ "JSONRECON_PAYMENT_MODE",
178
+ "JSONRECON_EVM_PRIVATE_KEY",
179
+ "JSONRECON_X402_SESSION_LIMIT_USDC",
180
+ ].some(prefix => message.startsWith(prefix));
181
+ return { error: allowed ? message : "JSON Recon request failed before a safe response was received." };
182
+ }
183
+ const server = new Server({ name: "jsonrecon-mcp", version: VERSION }, { capabilities: { tools: {} } });
184
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
29
185
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
30
186
  if (request.params.name === "extract_schema") {
31
187
  const url = String(request.params.arguments?.url);
32
188
  const schemaType = String(request.params.arguments?.schema_type || "auto");
33
189
  const mode = String(request.params.arguments?.mode || "full");
190
+ const route = mode === "lite" ? "/extract/lite" : "/extract";
34
191
  try {
35
- const endpointPath = mode === "lite" ? "/extract/lite" : "/extract";
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
- });
192
+ const response = await postPaid(route, { url, schema_type: schemaType });
45
193
  return {
46
194
  structuredContent: response.data,
47
- content: [
48
- {
49
- type: "text",
50
- text: JSON.stringify(response.data, null, 2),
51
- },
52
- ],
195
+ content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
53
196
  };
54
197
  }
55
198
  catch (error) {
56
- const errorMsg = error.response?.data?.error || error.message;
57
199
  return {
58
200
  isError: true,
59
- content: [
60
- {
61
- type: "text",
62
- text: `JsonRecon API Error: ${errorMsg}`,
63
- },
64
- ],
201
+ content: [{ type: "text", text: JSON.stringify(safeError(error), null, 2) }],
65
202
  };
66
203
  }
67
204
  }
68
205
  if (request.params.name === "verify_product") {
69
206
  const url = String(request.params.arguments?.url);
70
207
  try {
71
- const response = await axios.post(`${ENDPOINT}/extract/verified`, { url }, { headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" } });
208
+ const response = await postPaid("/extract/verified", { url });
72
209
  return {
73
210
  structuredContent: response.data,
74
211
  content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
75
212
  };
76
213
  }
77
214
  catch (error) {
78
- const body = error.response?.data;
79
- const errorMsg = body?.error || error.message;
80
215
  return {
81
216
  isError: true,
82
- content: [{ type: "text", text: JSON.stringify({ error: errorMsg, reason: body?.reason, refunded: body?.refunded }, null, 2) }],
217
+ content: [{ type: "text", text: JSON.stringify(safeError(error), null, 2) }],
83
218
  };
84
219
  }
85
220
  }
86
221
  if (request.params.name === "check_extractability") {
87
222
  const url = String(request.params.arguments?.url);
88
223
  try {
89
- const response = await axios.get(`${ENDPOINT}/check`, { params: { url } });
224
+ const response = await plainApi.get("/check", { params: { url } });
90
225
  return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
91
226
  }
92
227
  catch (error) {
93
- const errorMsg = error.response?.data?.error || error.message;
94
- return { isError: true, content: [{ type: "text", text: `JsonRecon API Error: ${errorMsg}` }] };
228
+ return {
229
+ isError: true,
230
+ content: [{ type: "text", text: JSON.stringify(safeError(error), null, 2) }],
231
+ };
95
232
  }
96
233
  }
97
234
  throw new Error(`Tool not found: ${request.params.name}`);
@@ -99,9 +236,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
99
236
  async function main() {
100
237
  const transport = new StdioServerTransport();
101
238
  await server.connect(transport);
102
- console.error("JsonRecon MCP server running on stdio");
239
+ console.error("JSON Recon MCP server running on stdio");
103
240
  }
104
- main().catch((error) => {
105
- console.error("Server error:", error);
241
+ main().catch(() => {
242
+ console.error("JSON Recon MCP server could not start. Check its non-secret configuration.");
106
243
  process.exit(1);
107
244
  });
package/package.json CHANGED
@@ -1,28 +1,32 @@
1
1
  {
2
2
  "name": "@jsonrecon/mcp-server",
3
- "version": "1.2.0",
4
- "description": "MCP tools for concise Schema.org extraction and Product verification",
3
+ "version": "1.3.1",
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": "^1.7.0"
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",