fanfou-mcp
A Model Context Protocol server that lets an AI assistant read Fanfou (饭否) timelines, search statuses, look up users, and — with explicit confirmation — post, delete, favorite and follow.
Account check:
verify_credentialsreturns the authenticated Fanfou account.Timelines:
home_timeline,user_timeline,mentions,public_timeline— read statuses withcount(1–60),since_id,max_id,pagefilters.Search:
search_public_timelineandsearch_user_timelinefor keyword search (a b= AND,a|b= OR).Lookups:
get_status(one status by id),get_context(reply chain/conversation),get_user(profile),favorites(a user's favorited statuses).Write actions:
post_status(post/reply/repost),delete_status,favorite(create/destroy),friendship(follow/unfollow) — each requiresconfirm=trueafter your explicit approval, and is unregistered entirely whenFANFOU_MCP_READONLY=1(the HTTP default).Run anywhere: stdio for local clients (Claude Desktop, Cursor), or Streamable HTTP
/mcpfor remote/serverless hosting, including multi-user instances where each person authorizes their own Fanfou account and receives a personal bearer token (read-only).
Exposes the Fanfou (饭否) REST API as MCP tools for reading and writing a user's timeline: fetching home, user, public and mention timelines, searching public statuses and within a user's statuses, retrieving individual statuses and their reply-chain context, looking up user profiles and favorites, plus posting, deleting and reposting statuses, favoriting/unfavoriting, and following/unfollowing users (write actions require explicit confirmation).
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@fanfou-mcp show me my home timeline"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
fanfou-mcp
A Model Context Protocol server for Fanfou (饭否). 让 Claude、Cursor 等 MCP 客户端直接读写你的饭否时间线。
fanfou-mcp exposes the Fanfou REST API as MCP tools so that an AI assistant can read your timeline,
search statuses, look up users and (optionally, with explicit confirmation) post, delete, favorite and follow.
It ships with two transports sharing the same tool set:
Transport | Entry point | Typical use |
stdio |
| Local MCP clients (Claude Desktop, Cursor, …) launching the server as a subprocess |
Streamable HTTP |
| Remote / serverless hosting, e.g. on Vercel — single-user (one bearer token) or multi-user (each user authorizes their own Fanfou account) |
Python ≥ 3.12, built on the official mcp Python SDK (FastMCP).
The Fanfou client itself uses only the standard library (no requests / oauthlib).
⚠️ Before you start: you need your own Fanfou app key
Fanfou has not accepted new API application registrations since 2020 (the developer program is whitelist-only). 饭否自 2020 年起不再开放新应用申请,新注册的应用无法通过审核,调用 API 会返回 401。
This project therefore does not include any consumer key. To use it you must bring:
A consumer key / secret of a Fanfou app that has already been approved (审核通过), and
An OAuth access token / secret for your account, obtained with that app (see OAuth authorization).
An app whose review status is "rejected" (已驳回) or pending will be able to complete the OAuth handshake,
but every API call (even /help/test.json) will fail with 401 参数错误!.
Related MCP server: twitter-mcp
Tools
Read-only tools (11)
Tool | Description |
| Return the authenticated account |
| Home timeline (statuses from people you follow); |
| A user's statuses ( |
| Statuses mentioning you |
| Latest public statuses |
| Keyword search across public statuses ( |
| Keyword search within one user's statuses |
| One status by id |
| Conversation context (reply chain) of a status |
| A user's profile ( |
| A user's favorited statuses |
Write tools (4)
Tool | Description |
| Post a status; supports |
| Delete one of your own statuses |
| Favorite ( |
| Follow ( |
Safety rails for write tools:
Every write tool requires
confirm=true. Without it, nothing is sent and the tool returns an error (post_statusalso returns apreviewof the text). Tool descriptions instruct the model to only setconfirm=trueafter the user has explicitly approved the exact action in chat.Set
FANFOU_MCP_READONLY=1to not register the write tools at all. This is the default for the HTTP server (http_app.py); the stdio server defaults to0(write tools available).
Responses are trimmed to the useful fields (id, text, author, timestamps, photo URL, repost, …) to keep
the model's context small. API errors are returned as {"error": "..."} rather than raised.
Architecture
fanfou_api.py Minimal Fanfou OAuth 1.0a client (HMAC-SHA1 signing, stdlib urllib)
creds.py Credential resolution: app key from env + per-request user token; multi-user token storage
kv.py Tiny KV client: Vercel KV / Upstash Redis REST API (stdlib), or in-memory for local dev
tools.py FastMCP instance and all 15 tool definitions; READONLY switch
server.py stdio entry point -> mcp.run()
http_app.py Streamable HTTP ASGI app -> POST /mcp, GET /healthz, bearer auth,
GET /authorize, GET /callback, POST /revoke (multi-user)Helper scripts:
File | Purpose |
| Launch the stdio server via |
| Serve |
| stdio smoke test: list tools, call a few read tools ( |
| HTTP smoke test against a local or remote |
| Multi-user end-to-end test (OAuth routes, per-user tokens, revoke) |
| Local stand-in for the KV REST API, for testing |
| Issue a personal token for an existing Fanfou access token (admin / migration) |
Notes on the HTTP app:
Stateless, JSON responses (
stateless_http=True,json_response=True) and a freshStreamableHTTPSessionManagerper request, so it does not depend on ASGI lifespan — safe for serverless.creds.current_principal/creds.current_user_tokenareContextVars set per request by the auth layer. In multi-user mode they carry the calling user's Fanfou token (looked up in KV); with the admin token, or over stdio, the owner token from env is used. Tools never see bearer tokens.
Implementation notes (Fanfou quirks)
These cost real debugging time; they are documented here so you don't have to rediscover them.
OAuth 1.0a: sign
http://, requesthttps://. Requests are sent tohttps://api.fanfou.com/..., but the OAuth signature base string must use thehttp://URL. Signing the https URL fails withInvalid signature. Expected basestring is GET&http%3A%2F%2F.... 签名 base string 必须用http://,实际请求走https://。The API gateway rejects
curl/*User-Agents.api.fanfou.comanswers an nginx404for anycurl/*UA, so the client sends its ownUser-Agent. Keep this in mind when testing by hand with curl (pass-A something-else).Requests use
mode=litewhere supported to get smaller payloads.Percent-encoding follows RFC 3986 (
quote(s, safe="~")) for both the signature and the request.
Configuration
Variable | Required | Description |
| yes | Consumer key of an approved Fanfou app |
| yes | Consumer secret |
| single-user | User access token (owner account; optional in multi-user mode) |
| single-user | User access token secret |
| HTTP only | Admin / owner bearer token ( |
| multi-user (option A) | Vercel KV / Upstash Redis REST credentials. Also accepts |
| multi-user (option B) | Redis URL (Vercel Marketplace Redis injects this; |
| multi-user | Public origin used to build the OAuth callback, e.g. |
| no |
|
| no |
|
| no |
|
Optional convenience for run.sh / dev_http.sh (environment variables always win):
~/.fanfou_app— an INI-like file with sections headed# <app name>containingFANFOU_API_KEY=.../FANFOU_API_SECRET=...; select the section withFANFOU_APP_SECTION(defaultdefault).FANFOU_TOKEN_FILE(default~/.fanfou_token) — a file containingFANFOU_OAUTH_TOKEN=.../FANFOU_OAUTH_TOKEN_SECRET=...lines.
Never commit these files or a .env (both are git-ignored).
Running locally (stdio)
Requires uv.
git clone https://github.com/Anthonyeef/fanfou-mcp.git
cd fanfou-mcp
uv sync
export FANFOU_API_KEY=<consumer_key>
export FANFOU_API_SECRET=<consumer_secret>
export FANFOU_OAUTH_TOKEN=<access_token>
export FANFOU_OAUTH_TOKEN_SECRET=<access_token_secret>
uv run python test_client.py --all # smoke testMCP client configuration
Claude Desktop (claude_desktop_config.json), Cursor (~/.cursor/mcp.json) and most other clients accept:
{
"mcpServers": {
"fanfou": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/fanfou-mcp", "python", "server.py"],
"env": {
"FANFOU_API_KEY": "<consumer_key>",
"FANFOU_API_SECRET": "<consumer_secret>",
"FANFOU_OAUTH_TOKEN": "<access_token>",
"FANFOU_OAUTH_TOKEN_SECRET": "<access_token_secret>",
"FANFOU_MCP_READONLY": "1"
}
}
}
}Drop FANFOU_MCP_READONLY (or set it to 0) if you want the write tools. Alternatively point command
at /absolute/path/to/fanfou-mcp/run.sh and let it load credentials from local files.
Local HTTP server
export MCP_AUTH_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
./dev_http.sh # serves http://127.0.0.1:8765/mcp
uv run python test_http_client.py # in another shell, same envRemote deployment (Vercel)
pyproject.toml declares the ASGI entry point for Vercel's Python runtime:
[tool.vercel]
entrypoint = "http_app:app"Create a project in your Vercel account/team. Framework preset: Python (not "Other"/null — with no preset Vercel expects functions under
/api). Root directory: this repo.Add environment variables (Production, mark as Sensitive):
FANFOU_API_KEY,FANFOU_API_SECRET,FANFOU_OAUTH_TOKEN,FANFOU_OAUTH_TOKEN_SECRET,MCP_AUTH_TOKEN(a fresh random string), and optionallyFANFOU_MCP_READONLY=1(already the HTTP default). Do not setFANFOU_MCP_ALLOW_NOAUTH.Deploy:
vercel deploy --prodfrom the repo, or connect the Git repository. Preview deployments sit behind Vercel Deployment Protection, so test against the production domain.Verify:
curl https://<your-project>.vercel.app/healthz MCP_AUTH_TOKEN=<token> uv run python test_http_client.py https://<your-project>.vercel.app/mcpConnect a client that supports remote MCP servers:
{ "mcpServers": { "fanfou": { "url": "https://<your-project>.vercel.app/mcp", "headers": { "Authorization": "Bearer <MCP_AUTH_TOKEN>" } } } }
Endpoints: POST /mcp (MCP Streamable HTTP, stateless, JSON), GET /healthz (returns
{"ok": true, "readonly": ...}). vercel.json requests the hkg1 region; Vercel may still run the
function elsewhere — the Fanfou API has been reachable from US regions as well.
Security: with the single-user model, anyone holding
MCP_AUTH_TOKENacts as your Fanfou account. Treat it like a password and rotate it if leaked. Keep the remote server read-only unless you need writes.
Hosting a multi-user instance / 托管多用户实例
One deployment can serve many people: everyone connects to the same /mcp endpoint, but each person
authorizes their own Fanfou account in the browser and gets a personal bearer token, so tools read
their own timeline. 同一个实例服务多人,每人授权自己的饭否账号、拿到自己的个人令牌。
How it works:
GET /authorize— the server requests a Fanfou request token (withoauth_callback=<public url>/callback), stores its secret in KV for 10 minutes, and redirects the browser to Fanfou's authorization page.The user logs in to Fanfou and approves the app.
GET /callback— Fanfou redirects back; the server consumes the request-token secret (one-shot), exchanges it for the user's access token, callsverify_credentials, generates a random personal token (ffm_…, 256 bits) and shows it once, together with a ready-to-paste MCP client config.POST /mcpwithAuthorization: Bearer ffm_…— the server looks the token up in KV and runs the tools with that user's Fanfou token. Unknown or revoked tokens get401.POST /revokewith the same bearer deletes the personal token.
Storage (Vercel KV / Upstash Redis, REST API):
Key | Value | TTL |
| request token secret | 10 min, deleted on use |
|
| none |
Personal tokens are never stored in clear, only their SHA-256. The KV does hold users' Fanfou access tokens, so treat the KV credentials as secrets.
Setup on Vercel: attach either a KV / Upstash store (injects KV_REST_API_URL + KV_REST_API_TOKEN)
or the official Redis Marketplace integration (injects REDIS_URL). Either one enables multi-user
mode; REST is preferred when both are set. Then set FANFOU_MCP_PUBLIC_URL, keep FANFOU_API_KEY /
FANFOU_API_SECRET (an approved app), and redeploy. MCP_AUTH_TOKEN + FANFOU_OAUTH_TOKEN[_SECRET] are
optional and keep working as an admin/owner login. See DEPLOY.md.
Things to consider before opening an instance to others:
All users share one consumer key: Fanfou rate limits apply to that app, and abuse by one user can affect everyone. Make sure your use of the key complies with Fanfou's terms.
Keep the instance read-only (
FANFOU_MCP_READONLY=1, the HTTP default).Anyone holding a personal token can read that user's Fanfou (including protected statuses they can see).
Local test without a real KV:
python dev/mock_upstash.py 8079 & # fake KV REST API
export KV_REST_API_URL=http://127.0.0.1:8079 KV_REST_API_TOKEN=dev
export FANFOU_API_KEY=... FANFOU_API_SECRET=... FANFOU_OAUTH_TOKEN=... FANFOU_OAUTH_TOKEN_SECRET=...
export MCP_AUTH_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
./dev_http.sh &
uv run python test_multiuser.py # end-to-end checks
# or by hand: open http://127.0.0.1:8765/authorize in a browser, approve on Fanfou,
# then: MCP_AUTH_TOKEN=<the ffm_ token shown> uv run python test_http_client.pyConnecting as a user / 作为用户如何授权接入
If someone runs a hosted instance for you (e.g. https://<instance>):
Open
https://<instance>/authorizein your browser, log in to Fanfou and click approve. 打开授权链接,登录饭否并同意授权。You land on a page showing your personal token (
ffm_…) and a config snippet. Copy it now — it is shown only once. 令牌只显示一次,请立即保存。Add the server to your MCP client:
{ "mcpServers": { "fanfou": { "url": "https://<instance>/mcp", "headers": { "Authorization": "Bearer <your personal token>" } } } }To disconnect:
curl -X POST https://<instance>/revoke -H "Authorization: Bearer <your personal token>", and/or revoke the app's authorization in your Fanfou account settings. Lost the token? Just authorize again to get a new one.
The hosted instance is read-only: it can read your timeline, mentions, favorites and search, but cannot post, delete, favorite or follow on your behalf.
OAuth authorization (oob PIN)
Fanfou uses OAuth 1.0a. For a personal setup the simplest way to get an access token is the out-of-band (PIN) flow. 用 oob PIN 方式拿 access token:
Request token — signed (consumer key/secret only, empty token secret) request with
oauth_callback=oob:GET https://fanfou.com/oauth/request_tokenResponse:
oauth_token=<request_token>&oauth_token_secret=<request_token_secret>Authorize — open in a browser while logged in to Fanfou, and approve:
https://fanfou.com/oauth/authorize?oauth_token=<request_token>&oauth_callback=oobFanfou shows a PIN (verifier).
Access token — signed with the request token/secret and
oauth_verifier=<PIN>:GET https://fanfou.com/oauth/access_tokenResponse:
oauth_token=<access_token>&oauth_token_secret=<access_token_secret>→ use them asFANFOU_OAUTH_TOKEN/FANFOU_OAUTH_TOKEN_SECRET.
The same quirks apply: sign the http://fanfou.com/oauth/... URL while sending the request over
https, and don't use a curl/* User-Agent. A minimal standalone helper:
# oauth_pin.py — usage: python oauth_pin.py (needs FANFOU_API_KEY / FANFOU_API_SECRET in env)
import base64, hashlib, hmac, os, secrets, time, urllib.parse as up, urllib.request as ur
CK, CS = os.environ["FANFOU_API_KEY"], os.environ["FANFOU_API_SECRET"]
q = lambda s: up.quote(str(s), safe="~")
def call(path, token="", token_secret="", **extra):
p = {"oauth_consumer_key": CK, "oauth_nonce": secrets.token_hex(8), "oauth_signature_method": "HMAC-SHA1",
"oauth_timestamp": str(int(time.time())), "oauth_version": "1.0", **extra}
if token:
p["oauth_token"] = token
norm = "&".join(f"{q(k)}={q(v)}" for k, v in sorted(p.items()))
base = "&".join(["GET", q(f"http://fanfou.com{path}"), q(norm)]) # sign http://
key = f"{q(CS)}&{q(token_secret)}".encode()
p["oauth_signature"] = base64.b64encode(hmac.new(key, base.encode(), hashlib.sha1).digest()).decode()
hdr = {"Authorization": "OAuth " + ", ".join(f'{k}="{q(v)}"' for k, v in p.items()),
"User-Agent": "fanfou-mcp-oauth/0.1"}
with ur.urlopen(ur.Request(f"https://fanfou.com{path}", headers=hdr), timeout=20) as r: # send https://
return dict(up.parse_qsl(r.read().decode()))
req = call("/oauth/request_token", oauth_callback="oob")
print("Open and approve:\n https://fanfou.com/oauth/authorize?oauth_token=%s&oauth_callback=oob" % req["oauth_token"])
pin = input("PIN: ").strip()
acc = call("/oauth/access_token", req["oauth_token"], req["oauth_token_secret"], oauth_verifier=pin)
print("FANFOU_OAUTH_TOKEN=%s\nFANFOU_OAUTH_TOKEN_SECRET=%s" % (acc["oauth_token"], acc["oauth_token_secret"]))Store the printed values somewhere private (a password manager, a chmod 600 file, or your host's
secret store). You can revoke the authorization from your Fanfou account settings at any time.
Roadmap
KV/Redis-backed per-user tokens + web OAuth callback flow, enabling multi-user hosting
MCP OAuth (authorization-server metadata) so clients can connect without pasting a token
Photo upload (
/photos/upload.json), direct messages, follower/friend lists
Contributions are welcome — please open an issue or PR.
Acknowledgements
Model Context Protocol and its Python SDK
License
MIT © Anthonyeef
Available Tools
15 toolsdelete_statusA
Delete one of the user's own statuses. WRITE ACTION: only call after the user has explicitly confirmed this exact action and content in chat. Requires confirm=true.
Inputs: status_id (required), confirm.
favoriteA
Favorite (action='create') or unfavorite (action='destroy') a status. WRITE ACTION: only call after the user has explicitly confirmed this exact action and content in chat. Requires confirm=true.
Inputs: status_id (required), action, confirm.
favoritesC
Read a user's favorited statuses (empty = the authenticated user).
Inputs: page, count, user_id. Returns: result
friendshipA
Follow (action='create') or unfollow (action='destroy') a user. WRITE ACTION: only call after the user has explicitly confirmed this exact action and content in chat. Requires confirm=true.
Inputs: user_id (required), action, confirm.
get_contextC
Get the conversation context (reply chain) of a status.
Inputs: status_id (required). Returns: result
get_statusC
Get one status by id.
Inputs: status_id (required).
get_userA
Get a user's profile by id (empty = the authenticated user).
Inputs: user_id.
home_timelineC
Read the authenticated user's home timeline (statuses from people they follow). count 1-60.
Inputs: page, count, max_id, since_id. Returns: result
mentionsB
Read statuses that mention the authenticated user. count 1-60.
Inputs: page, count, max_id, since_id. Returns: result
post_statusA
Post a new status (发饭). Optional in_reply_to_status_id / repost_status_id. WRITE ACTION: only call after the user has explicitly confirmed this exact action and content in chat. Requires confirm=true.
Inputs: status (required), confirm, repost_status_id, in_reply_to_status_id.
public_timelineC
Read the latest public statuses on Fanfou. count 1-60.
Inputs: count. Returns: result
search_public_timelineC
Search public statuses by keyword (supports 'a b' AND and 'a|b' OR).
Inputs: q (required), count, max_id. Returns: result
search_user_timelineB
Search within one user's statuses (user_id empty = the authenticated user).
Inputs: q (required), count, max_id, user_id. Returns: result
user_timelineB
Read a user's statuses. user_id empty = the authenticated user. count 1-60.
Inputs: page, count, max_id, user_id, +1 more. Returns: result
verify_credentialsA
Return the authenticated Fanfou account (read-only).
Inputs: no named parameters.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 15 tools
Most tools have clearly distinct resource+scope (home_timeline vs user_timeline vs public_timeline vs mentions are separated by audience, not confused). The one real overlap is verify_credentials versus get_user with an empty id, which both return the authenticated user, and the two search_* variants could be momentarily confused. Otherwise boundaries are clean.
The set mixes verb_noun (get_user, get_status, post_status, delete_status) with bare-noun reads (home_timeline, user_timeline, mentions, favorites) and bare-noun writes (favorite, friendship). The convention is readable and mostly predictable, but the inconsistent use of get_/search_ prefixes across reads keeps it from being fully uniform.
15 tools is at the upper edge of the well-scoped band but each one maps to a distinct Fanfou operation (timelines, search, status lifecycle, favorites, follow, context). Nothing feels redundant enough to cut. Slightly heavy but justified by the surface.
Core lifecycle is covered: read/search timelines, get status and reply context, post/delete, favorite/unfavorite, follow/unfollow. Gaps exist (no follower/following lists, no direct messages, no user search), but these are minor for the primary microblogging workflows an agent would run.
Maintenance
Related MCP Connectors
Hosted X (Twitter) MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Mastodon MCP — public Mastodon data via mastodon.social (no auth required)
Twitter/X read-only MCP server — 12 tools: search, users, tweets, followers, timelines, trends.
Talk to your public-facing AI from any MCP client — Claude, ChatGPT, Cursor, Cline, Windsurf.
Related MCP Servers
- AlicenseBqualityDmaintenanceMCP server for interacting with the X platform (Twitter) via MCP clients like Claude, Cursor AI, and Windsurf AI.2018 npm6MIT
- AlicenseNot gradedqualityCmaintenanceTwitter/X MCP server for the API v2, enabling users to manage profiles, posts, likes, retweets, and follows through natural language using a stateless Streamable HTTP transport with OAuth user access token authentication.14 npmISC
- AlicenseAqualityBmaintenanceEnables agents and scripts to drive a social publishing account through remote or local MCP tools: drafting, scheduling, approving and publishing posts, running durable background jobs with file attachments, and reading or editing Brand Memory, assets, and standing agents. Hosts can connect over OAuth with no API key, or run the stdio variant with a key for local disk tools.651MIT
- -licenseNot gradedqualityNot gradedmaintenanceA local read-only MCP server that lets MCP clients such as the Claude app fetch X/Twitter posts and search, Reddit posts, threads and search, and YouTube transcripts and search. It reads through your own saved logins using a fixed set of read-only commands, and cannot post, vote, follow or message.-