Our MCP product call returned HTTP 200 for a parent-ASIN failure. A parser that skips the tool status will fail later or emit an empty record. This walkthrough keeps transport, MCP, API, and task status apart, then saves the data needed for inspection.
Observed September 28, 2026. We did not inspect the Amazon pages against these responses; values are observations, not standing offers.
Step three: connect MCP and call the product tool
We completed initialize, the initialized notification, tools/list, and pangolinfo_capabilities over Streamable HTTP. The server identified itself as pangolinfo-mcp version 0.9.2 and exposed 21 tools. Discovery gives the interface available at connection time; a marketing-page tool count is not the contract your client should parse against.
This is a generic configuration example, not a file containing our credential. Client field conventions differ; consult the connection and authentication reference. Our experiment used a temporary client session and did not write the credential into project files or change a global client configuration.
{"mcpServers":{"pangolinfo":{"url":"https://mcp.pangolinfo.com/mcp","headers":{"Authorization":"Bearer <YOUR_API_KEY>"}}}}
The following tool arguments were used for the baseline. MCP places zipcode at the top of arguments. The REST route places it under bizContext. That small translation matters when moving a working agent lookup into application code.
{"name":"get_amazon_product","arguments":{"asin":"B0CMZFCQ6D","site":"amz_us","zipcode":"10041","format":"json"}}
For an agent, make the scope explicit: “Look up ASIN B0CMZFCQ6D on Amazon US with ZIP 10041. Call product details only. Return the title, raw price, seller, parent ASIN, RAM, and storage capacity. Record missing fields; do not add review collection.” A bounded request prevents a detail lookup from expanding into unrelated searches or paid review tasks.
How should connection errors be separated from product errors?
Our first Python urllib connection received a Cloudflare 403/1010 response describing a client-signature restriction. A standard curl HTTP client then reached authentication and completed the MCP handshake. This is an observation about this connection path, not a universal fix for 403 or evidence that the key was invalid.
Treat transport access, MCP initialization, argument validation, and product retrieval as separate layers. A credential error needs credential checks; BAD_INPUT needs changed arguments. The parent lookup's SERVER error called for checking the object or a bounded recheck. We stopped after its second failure despite the tool marking the error retriable.
Step four: make a REST request and locate the product result
For a script, the REST request below targets the same white phone as the MCP baseline. It uses content for the ASIN and leaves url empty. Keep parserName aligned with the page type. Do not supply a full URL that identifies one item while content identifies another.
{"url":"","parserName":"amzProductDetail","site":"amz_us","content":"B0CMZFCQ6D","format":"json","bizContext":{"zipcode":"10041"}}
The request goes to the documented /api/v1/scrape endpoint with bearer authentication and application/json content. Our REST result had HTTP 200, code 0, and pointCost 1.0, followed by data.json. MCP added a JSON-RPC envelope with a text block containing the API payload. Both successful paths then exposed task objects and product results.
Check transport first, then JSON-RPC error and MCP isError where present, then the API code, task code, and results. Validate returned identity before extracting values. Reading response.price skips every actual container. Treating HTTP 200 as the success condition would accept the malformed-input and parent errors as product data.
What does the captured JSON contain?
The excerpt below comes from the 10041 MCP response. It omits other keys and array entries for readability without inventing values. It records this September 28 observation, not a current offer guarantee.
{
"asin": "B0CMZFCQ6D",
"parentAsin": "B0GP8D698X",
"title": "Apple iPhone 15 Pro, 512GB, White Titanium - Unlocked (Renewed)",
"price": "$619.00",
"size": "8 GB",
"color": "",
"seller": {
"hasLink": true,
"name": "Prime Renewed Store",
"link": "/gp/help/seller/at-a-glance.html/ref=dp_merchant_link?ie=UTF8&seller=A1NY8YUA22BXMM&asin=B0CMZFCQ6D&ref_=dp_merchant_link",
"id": "A1NY8YUA22BXMM"
},
"delivery": {
"deliveryTime": "Tomorrow, September 29",
"fastestDelivery": ""
},
"attributes": [
{
"value": "8 GB",
"key": "RAM Memory Installed"
},
{
"value": "512 GB",
"key": "Memory Storage Capacity"
},
{
"value": "White Titanium",
"key": "Color"
}
]
}
The result object contained 51 top-level keys, 49 attributes, and three images entries, plus other image arrays serving different purposes. Those counts do not establish that all fields were populated or every business requirement passed. Choose the image array your output requires, retaining provenance and handling duplicates or size constraints in the application.
RAM Memory Installed was 8 GB, Memory Storage Capacity was 512 GB, and Color was White Titanium. Top-level size was also 8 GB, while color was empty. Mapping size to phone storage would be wrong; declaring color unavailable would overlook its named attribute. Build a mapping by attribute name and preserve the source array rather than depending on element positions.
The title also specified Renewed. That condition belongs to the purchased item and should not disappear when a comparison joins new and renewed offers. Save seller.id and seller.name alongside price. Removing the dollar symbol and the seller may make a spreadsheet shorter, but it also removes evidence needed to interpret differences.
Why are 18 variation entries not 18 distinct products?
The baseline's variantDetails array held 18 entries but only 15 distinct ASIN values. The selected child appeared in several dimensions such as capacity, color, carrier, or product condition. This is option information, not an accepted independent-product count. Empty option prices should remain empty rather than inherit the selected product's price.
The black child B0CMZ5KBNS returned $617.95 from Cell Surfers in its own call and shared the parent's identifier. That supports querying the two purchase options apart; it does not prove whole-family coverage. A dedicated variation workflow would need a known comparison set and its own acceptance conditions.
The baseline also contained 13 review entries, while the 90001 response had reviews set to null. That is not evidence that the second destination has no reviews. For pagination, filters, and review attribution, evaluate Amazon Review API as a separate task rather than treating detail-page snippets as a review archive.
Reading MCP and REST product responses; based on responses observed September 28, 2026.
Step five: save results with Python, JSON, and CSV
The accompanying product_by_asin.py example supports live REST retrieval and offline inspection of captured MCP or REST records. It reads the key from the environment and sends curl configuration through standard input. It saves the raw record before parsing, so rejected output can still be investigated. Live mode requires Python 3.10 or later and curl.
python3 examples/product_by_asin.py --asin B0CMZFCQ6D --site amz_us --zipcode 10041 --output captures
To reproduce the article's saved observation without another remote request, use fixture mode. It checks the original request context against the CLI arguments and labels the output fixture. A replay is not a new live query, and a saved 10041 response must not be relabeled as a new 90001 observation.
python3 examples/product_by_asin.py --asin B0CMZFCQ6D --site amz_us --zipcode 10041 --fixture 04_元数据/evidence/product-us-10041.json --output replay
This extraction function is the core of the supplied script. The full file also handles requests, raw evidence, JSON output, and CSV. It accepts task entries encoded as objects or JSON strings; our live responses used objects.
import json
def extract(response):
if 'error' in response:
raise ValueError('JSON-RPC error; inspect saved evidence')
if 'result' in response:
result = response['result']
if result.get('isError'):
raise ValueError('MCP tool error; inspect saved evidence')
parts = [x['text'] for x in result.get('content', []) if x.get('type') == 'text']
if len(parts) != 1:
raise ValueError('Unexpected MCP content shape')
response = json.loads(parts[0])
if response.get('code') != 0:
raise ValueError('API business error')
tasks = response.get('data', {}).get('json')
if not isinstance(tasks, list) or not tasks:
raise ValueError('Missing task list')
rows = []
for task in tasks:
task = json.loads(task) if isinstance(task, str) else task
if not isinstance(task, dict) or task.get('code') != 0:
raise ValueError('Task error')
items = task.get('data', {}).get('results')
if not isinstance(items, list) or not items or not all(isinstance(x, dict) for x in items):
raise ValueError('Missing product results')
rows.extend(items)
return rows
"""Fetch a single ASIN through REST, or inspect a saved MCP/REST capture.
Requires Python 3.10+ and curl for live requests. No credentials in files.
"""
import argparse
import csv
import json
import os
from pathlib import Path
import re
import subprocess
from datetime import datetime, timezone
from uuid import uuid4
def extract(response):
if 'error' in response:
raise ValueError('JSON-RPC error; inspect saved evidence')
if 'result' in response:
result = response['result']
if result.get('isError'):
raise ValueError('MCP tool error; inspect saved evidence')
parts = [x['text'] for x in result.get('content', []) if x.get('type') == 'text']
if len(parts) != 1:
raise ValueError('Unexpected MCP content shape')
response = json.loads(parts[0])
if response.get('code') != 0:
raise ValueError('API business error')
tasks = response.get('data', {}).get('json')
if not isinstance(tasks, list) or not tasks:
raise ValueError('Missing task list')
rows = []
for task in tasks:
task = json.loads(task) if isinstance(task, str) else task
if not isinstance(task, dict) or task.get('code') != 0:
raise ValueError('Task error')
items = task.get('data', {}).get('results')
if not isinstance(items, list) or not items or not all(isinstance(x, dict) for x in items):
raise ValueError('Missing product results')
rows.extend(items)
return rows
def csv_safe(value):
text = '' if value is None else str(value)
return "'" + text if text.lstrip().startswith(('=', '+', '-', '@')) else text
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--asin', required=True)
parser.add_argument('--site', default='amz_us')
parser.add_argument('--zipcode', required=True)
parser.add_argument('--fixture', type=Path)
parser.add_argument('--output', type=Path, default=Path('captures'))
args = parser.parse_args()
asin = args.asin.strip().upper()
if not re.fullmatch(r'[A-Z0-9]{10}', asin):
parser.error('ASIN must contain 10 letters/digits')
args.output.mkdir(parents=True, exist_ok=True)
if args.fixture:
record = json.loads(args.fixture.read_text())
req = record.get('request', {})
context = req.get('params', {}).get('arguments', {}) if req.get('method') == 'tools/call' else req
captured_asin = context.get('asin', context.get('content'))
captured_zip = context.get('zipcode', context.get('bizContext', {}).get('zipcode'))
if (captured_asin, context.get('site'), captured_zip) != (asin, args.site, args.zipcode):
raise ValueError('Fixture request differs from CLI context')
mode = 'fixture'
else:
key = os.environ.get('PANGOLINFO_API_KEY', '')
if not key or '\n' in key or '\r' in key:
parser.error('Set PANGOLINFO_API_KEY in the process environment')
payload = {'url': '', 'parserName': 'amzProductDetail', 'site': args.site,
'content': asin, 'format': 'json', 'bizContext': {'zipcode': args.zipcode}}
started = datetime.now(timezone.utc).isoformat()
config = 'url = "https://scrapeapi.pangolinfo.com/api/v1/scrape"\nrequest = "POST"\n'
for h in ['Content-Type: application/json', 'Authorization: Bearer ' + key]:
config += 'header = ' + json.dumps(h) + '\n'
config += 'data = ' + json.dumps(json.dumps(payload)) + '\n'
env = {k: v for k, v in os.environ.items() if k != 'PANGOLINFO_API_KEY'}
proc = subprocess.run(['curl', '-sS', '--max-time', '60', '--config', '-',
'--write-out', '\n%{http_code}'], input=config,
text=True, capture_output=True, env=env)
raw = proc.stdout.replace(key, '[REDACTED]')
body, _, status = raw.rpartition('\n')
record = {'request': payload, 'started_at_utc': started,
'received_at_utc': datetime.now(timezone.utc).isoformat(),
'http_status': int(status) if status.isdigit() else 0,
'curl_exit': proc.returncode, 'raw_body': body}
try:
record['response'] = json.loads(body)
except ValueError:
record['response'] = {}
mode = 'live'
stem = args.output / (mode + '-' + uuid4().hex)
stem.with_suffix('.raw.json').write_text(json.dumps(record, ensure_ascii=False, indent=2))
if record.get('http_status') != 200 or record.get('curl_exit', 0):
raise ValueError('Transport failure; inspect saved evidence')
rows = extract(record['response'])
outputs = []
for item in rows:
if item.get('asin') != asin:
raise ValueError('Returned ASIN mismatch; inspect saved evidence')
attrs = {}
for a in item.get('attributes') or []:
attrs.setdefault(a.get('key'), []).append(a.get('value'))
outputs.append({'mode': mode, 'requested_asin': asin, 'returned_asin': item['asin'],
'site': args.site, 'requested_zipcode': args.zipcode,
'received_at_utc': record.get('received_at_utc'),
'source_captured_at': None, 'destination_verified': None,
'title': item.get('title'), 'price_raw': item.get('price'),
'size_raw': item.get('size'), 'attributes_by_name': attrs,
'ready_for_price_comparison': False})
stem.with_suffix('.json').write_text(json.dumps(outputs, ensure_ascii=False, indent=2))
columns = ['requested_asin', 'returned_asin', 'site', 'requested_zipcode', 'title', 'price_raw']
with stem.with_suffix('.csv').open('w', newline='', encoding='utf-8-sig') as handle:
writer = csv.DictWriter(handle, fieldnames=columns)
writer.writeheader()
writer.writerows({k: csv_safe(row.get(k)) for k in columns} for row in outputs)
print(json.dumps({'mode': mode, 'records': len(outputs), 'output_stem': str(stem)}))
if __name__ == '__main__':
main()
The normalized output retains raw price, raw size, named attributes, requested destination, and client receipt time. Source capture time and destination verification remain null because the sample does not establish them. ready_for_price_comparison remains false: structural parsing and identity checks do not settle offer comparability.
Duplicate attribute names retain multiple values instead of overwriting an earlier one. Missing values do not become zero. The CSV is a small viewing projection rather than a lossless substitute for JSON; it also escapes leading characters that spreadsheet software may treat as formulas. Raw evidence stays unchanged. These are application choices, not additional fields claimed to exist in the API response.
How should one lookup become a repeatable job?
This product tool takes one ASIN. For a list, deduplicate, queue requests, and save each result; a client loop is not proof of a native batch endpoint. Include marketplace, ASIN, ZIP, and requested format in the task identity so deduplication does not merge observations with different conditions.
Start with restrained concurrency and adjust against the service agreement and observed errors. Separate completed tasks from invalid inputs, uncertain transport outcomes, rechecks, and records needing investigation. BAD_INPUT returns to input correction. A timeout leaves the outcome uncertain. The parent error in this run stopped after one recheck rather than entering an unbounded retry loop.
The three normal MCP child observations and one REST comparison each declared pointCost 1. The constructed identifier also declared 1, making five points across those five response fields. The parent and validation errors did not provide a value that establishes their charge. We did not inspect billing, so five is not a verified total debit. Use the pricing page and account records for current charges.
The three normal MCP calls took about 4.47–5.22 seconds on this client; the REST comparison took about 5.29 seconds. These are individual end-to-end timings, not backend processing measurements. Sequential requests with uncontrolled network and cache conditions cannot rank the speed of MCP against REST.
Observed child-product prices and sellers; sequential requests on September 28, 2026. The price difference cannot be attributed to ZIP alone.
What should you try next?
Start with one child ASIN and fixed marketplace and ZIP. Use Amazon Scraper API or Amazon Data MCP, then check the product reference. Consult pricing for current terms and case studies for separate project applications.


Top comments (0)