Ask an AI coding assistant "what are people saying about oat-milk lattes on Xiaohongshu this week?" and it will either guess or tell you it can't browse. The fix is an MCP server that exposes real data endpoints as tools. This walkthrough connects the Qovia MCP server (social, e-commerce and web data across Douyin, Xiaohongshu, Weibo, Bilibili, WeChat Official Accounts, TikTok, YouTube, X, Reddit and more) to Claude Code, Cursor and VS Code, then shows how to drive the same server from a plain Python script so your research is reproducible.

Disclosure: I build Qovia, and this article was drafted with AI assistance and checked by hand against the live docs and server.

What the server looks like

  • URL: https://api.qovia.ai/mcp
  • Transport: Streamable HTTP, stateless (each JSON-RPC request stands alone, answered with JSON; no session).
  • Auth: the same key as the REST API, Authorization: Bearer sk-qovia-....

By default it exposes only four tools, so it doesn't flood the model's context with 150+ endpoint schemas:

Tool What it does Needs key
search_endpoints Find endpoints by keyword (English or Chinese) and/or platform slug; returns ids, titles, price per call, live status No
get_endpoint One endpoint in full: description, JSON Schema params, example params, response example, price No
call_endpoint Run an endpoint (endpoint_id + params); returns data plus billing meta Yes
get_account Balance, plan and rate limit (not charged) Yes

The model's loop is: search, read the schema, call. Discovery is free and keyless, so you can explore the catalog before paying for anything. New accounts get 50 trial calls, and only successful, non-empty results are billed.

Connect a client

Create a key at https://qovia.ai/console/?tab=keys and keep it in an environment variable, not in your repo.

Claude Code

export QOVIA_KEY=sk-qovia-... # your own key claude mcp add --transport http qovia https://api.qovia.ai/mcp \ --header "Authorization: Bearer $QOVIA_KEY" 

Cursor (~/.cursor/mcp.json)

{ "mcpServers": { "qovia": { "url": "https://api.qovia.ai/mcp", "headers": { "Authorization": "Bearer sk-qovia-..." } } } } 

VS Code (.vscode/mcp.json) prompts for the key on first use and never writes it to the file, which makes it safe to commit:

{ "servers": { "qovia": { "type": "http", "url": "https://api.qovia.ai/mcp", "headers": { "Authorization": "Bearer ${input:qovia-key}" } } }, "inputs": [ { "type": "promptString", "id": "qovia-key", "description": "Qovia API key", "password": true } ] } 

Claude Desktop uses OAuth for remote connectors, so bridge with mcp-remote:

{ "mcpServers": { "qovia": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.qovia.ai/mcp", "--header", "Authorization:${QOVIA_AUTH}"], "env": { "QOVIA_AUTH": "Bearer sk-qovia-..." } } } } 

Restart the client and qovia shows up in the tool list.

Optional: direct tools for one platform

If you work on one platform all day, you can skip the search step. Append ?tools= with platform slugs (or ?tools=all):

https://api.qovia.ai/mcp?tools=xiaohongshu,weibo 

Every live endpoint on those platforms becomes its own tool, named after its id with dots turned into underscores (raw.xiaohongshu.search becomes raw_xiaohongshu_search). The four default tools stay. Keep the list short: each tool adds its schema to the context.

Prompts that work well

Research prompts go best when they name the platform, the time window and the shape of the output you want:

Using the qovia tools, search Xiaohongshu for notes about "燕麦拿铁" published this week, sorted by most liked. Give me a table of the top 10 with title, likes, saves, comments and author, then summarise the three recurring complaints.

A typical tool trace:

search_endpoints {"query": "xiaohongshu search"} get_endpoint {"endpoint_id": "raw.xiaohongshu.search"} call_endpoint {"endpoint_id": "raw.xiaohongshu.search", "params": {"keyword": "燕麦拿铁", ...}} 

get_endpoint is what keeps the model honest: it reads the real parameter schema (for example, Xiaohongshu search V4 takes note_type of all|video|image and note_time of all|day|week) instead of inventing flags. Unknown parameters are rejected with 400 invalid_params, which the model can see and correct.

Some other prompts worth trying:

  • "Find the WeChat Official Accounts that publish about cross-border payments, then list their latest articles." (wechat platform)
  • "Pull the Weibo hot search list and tell me which entries relate to EVs."
  • "Compare comments on this Bilibili video and this YouTube video on the same product."

Each call_endpoint result includes charged_usd and your balance, so you can ask the assistant to keep a running cost total.

Do the same thing from Python

Chat is great for exploration, but once a query is useful you want it in a script. Because the server is stateless Streamable HTTP, you don't even need an MCP SDK: a JSON-RPC POST per call is enough.

# qovia_mcp.py - minimal client for the Qovia MCP server import json import os import requests MCP_URL = "https://api.qovia.ai/mcp" HEADERS = { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", } if os.environ.get("QOVIA_KEY"): HEADERS["Authorization"] = f"Bearer {os.environ['QOVIA_KEY']}" _id = 0 def rpc(method, params=None): global _id _id += 1 body = {"jsonrpc": "2.0", "id": _id, "method": method} if params is not None: body["params"] = params r = requests.post(MCP_URL, headers=HEADERS, json=body, timeout=60) r.raise_for_status() msg = r.json() if "error" in msg: raise RuntimeError(msg["error"]) return msg["result"] def tool(name, **arguments): result = rpc("tools/call", {"name": name, "arguments": arguments}) if result.get("isError"): raise RuntimeError(result["content"][0]["text"]) # Prefer structured output; fall back to the text block.  if "structuredContent" in result: return result["structuredContent"] return json.loads(result["content"][0]["text"]) if __name__ == "__main__": # 1. Discovery: free, no key needed  for t in rpc("tools/list")["tools"]: print("tool:", t["name"]) found = tool("search_endpoints", query="xiaohongshu search") for ep in found["endpoints"]: print(ep["id"], ep["price_usd_per_call"], "live" if ep["available"] else "soon") detail = tool("get_endpoint", endpoint_id="raw.xiaohongshu.search") print(json.dumps(detail, ensure_ascii=False)[:600]) 

Run it without a key first; tools/list, search_endpoints and get_endpoint all work anonymously:

tool: search_endpoints tool: get_endpoint tool: call_endpoint tool: get_account raw.xiaohongshu.search 0.005 live raw.xiaohongshu.search_v4 0.02536 live raw.pgy.note_search 0.01 live raw.pgy.note_search_v2 0.02536 live 

Now add a paid call. Xiaohongshu note search returns the platform's own result: items, each with model_type and a note (id, title, liked_count, collected_count, comments_count, user, ...). There is no has_more; you page until a page comes back empty, and empty pages are not charged.

# xhs_top_notes.py import os from qovia_mcp import tool assert os.environ.get("QOVIA_KEY"), "set QOVIA_KEY first" print(tool("get_account")) # balance and rate limit, not charged  KEYWORD = "燕麦拿铁" notes = [] for page in range(1, 4): out = tool("call_endpoint", endpoint_id="raw.xiaohongshu.search", params={"keyword": KEYWORD, "page": page}) data = out.get("data") or {} items = [i["note"] for i in data.get("items", []) if i.get("model_type") == "note"] if not items: break notes.extend(items) print(f"page {page}: {len(items)} notes, charged {out.get('meta', {}).get('charged_usd')}") notes.sort(key=lambda n: int(n.get("liked_count") or 0), reverse=True) for n in notes[:10]: print(f"{n.get('liked_count'):>7} likes {n.get('collected_count'):>6} saves " f"{n.get('title') or '(no title)'} @{n['user']['nickname']}") 

The shape of call_endpoint's output mirrors the REST envelope (data plus meta with charged_usd). If your version prints it under a different key, print(out.keys()) once and adjust; the point is that what the assistant saw in chat and what your script sees are the same data.

Handling errors and limits

  • Errors come back as {"error": {"code", "message", ...}} and are never charged. Branch on code: invalid_params (fix your params), rate_limited / endpoint_busy (back off and honour Retry-After), insufficient_balance, upstream_timeout (retry later).
  • The account limit is 60 requests per minute (600 after the first top-up). A three-page loop is fine; a 500-keyword sweep needs a time.sleep.
  • Keep keys out of prompts and committed configs. The VS Code inputs pattern and environment variables are the easy wins.

When to use MCP vs. the REST API

Use the MCP server when a person is asking open questions and the model should pick the endpoint. Use the REST API (GET https://api.qovia.ai/v1/raw/xiaohongshu/search?keyword=...) in production jobs where the endpoint is fixed and you want one less moving part. Calls are billed identically either way, so prototyping in Claude or Cursor and then hardening in Python costs nothing extra.

Docs: MCP setup, API reference, and the whole catalog as one file for your model's context at llms-full.txt.