nftdata-mcp 1.0.0 → 1.0.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.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  An MCP server that exposes the [NFT Data](https://www.nftdata.app/) API to AI clients (Claude Desktop, Cursor, etc.) as a single tool. It handles the x402 payment flow automatically.
4
4
 
5
+ - **npm:** https://www.npmjs.com/package/nftdata-mcp
6
+ - **MCP Registry:** https://registry.modelcontextprotocol.io/?q=nftdata
7
+ - **NFT Data GitHub Repository:** https://github.com/Nomad07/nft-data
8
+
5
9
  ## Tool
6
10
 
7
11
  ### `get_collection_data`
@@ -25,8 +29,6 @@ Each call costs **$0.002** (USDC on Base, paid via x402).
25
29
 
26
30
  ## Quick start (npx)
27
31
 
28
- > Works only after the package has been published to npm. Until then, use "Run from source" below.
29
-
30
32
  Add to `claude_desktop_config.json`:
31
33
 
32
34
  ```json
@@ -45,8 +47,6 @@ Add to `claude_desktop_config.json`:
45
47
 
46
48
  Restart Claude Desktop, then ask for example: "What is the floor price and 7-day volume of azuki?"
47
49
 
48
- If the package is published under a scope, use that name instead (e.g. `@your-scope/nftdata-mcp`).
49
-
50
50
  ## Run from source
51
51
 
52
52
  ```bash
@@ -97,7 +97,7 @@ npm test # offline unit tests, no network and no payments
97
97
 
98
98
  - Sends `POST /api/get-collection-data` with body `{"collection": "<slug>"}`.
99
99
  - The slug is trimmed and lowercased, then must match `^[a-z0-9][a-z0-9_-]*$` (max 100 characters); otherwise no request is made.
100
- - Only Base mainnet (`eip155:8453`) is registered with the x402 client. Before signing, every payment challenge is checked: scheme `exact`, network Base mainnet, token USDC on Base, amount at most 2000 units ($0.002), and recipient (must be `0x344143199642e87320785823c20c2df107d17534`). Anything else is refused without signing.
100
+ - Only Base mainnet (`eip155:8453`) is registered with the x402 client. Before signing, every payment challenge is checked: scheme `exact`, network Base mainnet, token USDC on Base, amount exactly 2000 atomic units ($0.002), and recipient (must be `0x344143199642e87320785823c20c2df107d17534`). Anything else is refused without signing.
101
101
  - Each signed payment counts against `NFTDATA_MAX_REQUESTS`, including payments for requests that later fail or time out.
102
102
  - No automatic retries: one tool call makes at most one payment attempt.
103
103
  - Caches successful responses for 5 minutes per slug. Concurrent identical calls share a single in-flight request.
package/dist/lib.js CHANGED
@@ -108,8 +108,27 @@ export class SpendLimiter {
108
108
  }
109
109
  export const ok = (text) => ({ content: [{ type: "text", text }] });
110
110
  export const fail = (text) => ({ isError: true, content: [{ type: "text", text }] });
111
+ // Appends a payment receipt line to a successful tool result.
112
+ // txHash must already be validated (0x + 64 hex chars).
113
+ export function appendPaymentInfo(result, txHash) {
114
+ if (result.isError)
115
+ return result;
116
+ const receipt = `\n\nPayment: ${txHash}` +
117
+ `\nVerify: https://basescan.org/tx/${txHash}`;
118
+ return ok(result.content[0].text + receipt);
119
+ }
120
+ // Validates a tx hash string. Returns the hash if it looks like a valid
121
+ // Ethereum tx hash (0x + 64 hex chars), or null otherwise.
122
+ // This function has no x402 imports so it can be tested offline.
123
+ export function validateTxHash(raw) {
124
+ if (typeof raw !== "string")
125
+ return null;
126
+ return /^0x[0-9a-fA-F]{64}$/.test(raw) ? raw : null;
127
+ }
111
128
  // Cache (5 min per slug) + reuse of in-flight requests so concurrent identical calls
112
129
  // produce a single (paid) request. Failures are never cached.
130
+ // The loader returns { result, txHash }. Only result.content[0].text (the collection
131
+ // body) is cached; txHash is never stored, so cache hits always return txHash: null.
113
132
  export class CachedFetcher {
114
133
  cache = new Map();
115
134
  inflight = new Map();
@@ -122,23 +141,23 @@ export class CachedFetcher {
122
141
  async get(slug) {
123
142
  const hit = this.cache.get(slug);
124
143
  if (hit && this.now() - hit.at < CACHE_TTL_MS)
125
- return ok(hit.body);
144
+ return { result: ok(hit.body), txHash: null };
126
145
  if (hit)
127
146
  this.cache.delete(slug);
128
147
  const pending = this.inflight.get(slug);
129
148
  if (pending)
130
149
  return pending;
131
150
  const p = this.load(slug)
132
- .then((res) => {
133
- if (!res.isError) {
151
+ .then((lr) => {
152
+ if (!lr.result.isError) {
134
153
  if (this.cache.size >= CACHE_MAX_ENTRIES) {
135
154
  const oldest = this.cache.keys().next().value;
136
155
  if (oldest !== undefined)
137
156
  this.cache.delete(oldest);
138
157
  }
139
- this.cache.set(slug, { at: this.now(), body: res.content[0].text });
158
+ this.cache.set(slug, { at: this.now(), body: lr.result.content[0].text });
140
159
  }
141
- return res;
160
+ return lr;
142
161
  })
143
162
  .finally(() => {
144
163
  this.inflight.delete(slug);
package/dist/server.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
5
+ import { x402Client, wrapFetchWithPayment, decodePaymentResponseHeader } from "@x402/fetch";
6
6
  import { ExactEvmScheme } from "@x402/evm/exact/client";
7
7
  import { privateKeyToAccount } from "viem/accounts";
8
- import { BASE_MAINNET, CachedFetcher, REFUSAL_PREFIX, SpendLimiter, fail, isValidPrivateKey, mapError, mapResponse, normalizeSlug, parseConfig, validateRequirements, withTimeout, } from "./lib.js";
8
+ import { BASE_MAINNET, CachedFetcher, REFUSAL_PREFIX, SpendLimiter, appendPaymentInfo, fail, isValidPrivateKey, mapError, mapResponse, normalizeSlug, parseConfig, validateRequirements, validateTxHash, withTimeout, } from "./lib.js";
9
9
  let config;
10
10
  try {
11
11
  config = parseConfig(process.env);
@@ -73,12 +73,33 @@ async function load(slug) {
73
73
  redirect: "error",
74
74
  signal,
75
75
  });
76
- return { status: r.status, body: await r.text(), retryAfter: r.headers.get("Retry-After") };
76
+ // Extract the payment response header before consuming the body.
77
+ const paymentHeader = r.headers.get("PAYMENT-RESPONSE") ?? r.headers.get("X-PAYMENT-RESPONSE");
78
+ return {
79
+ status: r.status,
80
+ body: await r.text(),
81
+ retryAfter: r.headers.get("Retry-After"),
82
+ paymentHeader,
83
+ };
77
84
  }, config.timeoutMs);
78
- return mapResponse(res.status, res.body, res.retryAfter);
85
+ const result = mapResponse(res.status, res.body, res.retryAfter);
86
+ // Decode the payment header and extract the tx hash only on success.
87
+ // Any failure (malformed header, missing field, wrong format) is silently
88
+ // ignored — the collection result is still returned without payment info.
89
+ let txHash = null;
90
+ if (!result.isError && res.paymentHeader) {
91
+ try {
92
+ const settle = decodePaymentResponseHeader(res.paymentHeader);
93
+ txHash = validateTxHash(settle.transaction);
94
+ }
95
+ catch {
96
+ // Malformed header: proceed without payment info.
97
+ }
98
+ }
99
+ return { result, txHash };
79
100
  }
80
101
  catch (e) {
81
- return mapError(e);
102
+ return { result: mapError(e), txHash: null };
82
103
  }
83
104
  }
84
105
  const fetcher = new CachedFetcher(load);
@@ -95,6 +116,7 @@ server.tool("get_collection_data", "Get OpenSea NFT collection market data in on
95
116
  if (slug === null) {
96
117
  return fail("Invalid slug: use only letters, digits, '_' and '-' (max 100 characters).");
97
118
  }
98
- return fetcher.get(slug);
119
+ const { result, txHash } = await fetcher.get(slug);
120
+ return txHash ? appendPaymentInfo(result, txHash) : result;
99
121
  });
100
122
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nftdata-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "MCP server for the NFT Data API: OpenSea collection market data, paid per call via x402",
5
5
  "mcpName": "io.github.Nomad07/nftdata-mcp",
6
6
  "type": "module",