How to use this brief: Paste it into a fresh Claude Code session in an empty directory (e.g.
~/tools/perplexity-agent-mcp/). Claude Code should first verify the live API against the doc links in the Toolbox, then implement. Everything needed is below — the brief is self-contained.
Build a single-file, zero-third-party-dependency MCP server that gives an MCP client
(Claude Desktop / Cowork / Claude Code) one tool to run research through Perplexity's
Agent API (POST /v1/agent) and get back a synthesized, cited answer.
Success looks like: I drop one Python file into my MCP config, restart the client, and a
perplexity_agent tool appears that round-trips a query to Perplexity's agent and returns
the answer plus its sources. I can read the entire file in five minutes and trust it —
that auditability is the whole point of doing this instead of pulling an npm package. The
server is the trust boundary, so it must have no foreign supply chain to attack.
- The existing Perplexity MCP on this machine (
@perplexity-ai/mcp-server) only wraps the Sonar chat models + Search API. It does not reach the Agent API. This new server fills exactly that gap and runs alongside it. - Critical endpoint fact — do not get this wrong: The Agent API is reached at
POST https://api.perplexity.ai/v1/agent. Perplexity's OpenAI-compatibility alias is the Responses API (/v1/responses), not Chat Completions. A normalchat.completionscall routes to Sonar, not the agent. To avoid this trap entirely, call the raw/v1/agentendpoint directly — do not build on any OpenAI chat-completions abstraction. - Security posture is the reason this is bespoke: the API key must live only as an environment variable in the MCP server's config, never in code, logs, or model context.
- The owner is a senior engineer and will code-review the file before running it. Optimize for a reviewer who wants to be certain the file does nothing surprising.
perplexity_agent_mcp.py— the server. Python 3.9+, standard library only. Single file, heavily commented, readable top-to-bottom, target < ~180 lines.- A short
README.md(or a comment header block) with: what it does, the required env var, how to run it, and the exact client config snippet (below). - A self-test recipe — a copy-pasteable shell snippet that pipes a manual JSON-RPC
initialize→tools/list→tools/callsequence into the server and shows it working, so the server can be validated without any client.
- Auditability over cleverness. Boring, obvious, linear code beats abstraction.
- Zero dependencies over convenience. If a stdlib solution exists, use it. No
pip install. - Fail loud and safe over silent partial success. Surface errors as clean MCP tool errors; never leak the key or a raw stack trace into model-visible output.
- Verify against live docs over trusting this brief. API field names and model IDs drift; this brief is a July-2026 snapshot. Confirm before coding.
- Stdlib only:
json,os,sys,urllib.request/urllib.error,typing. No third-party packages, norequirements.txt, nopackage.json, no MCP SDK. - Single outbound host:
https://api.perplexity.aionly, HTTPS with default certificate verification on. No other network calls, no telemetry, no analytics, no auto-update. - Key handling: read from env
PERPLEXITY_API_KEY. If unset/empty, return a clear tool error. Never print, log, or echo the key or theAuthorizationheader. - Timeouts: set a request timeout (~120 s — the
high/xhighpresets are slow). On timeout or non-2xx, return a readable error, not a crash. - No global state / no files written beyond stdout/stderr. stderr may carry minimal, key-free diagnostic logging; stdout is reserved exclusively for JSON-RPC frames.
Expose one tool (keep it lean; more can come later):
- name:
perplexity_agent - description: "Run a research query through Perplexity's Agent API (multi-step web research with citations). Use for deep or multi-hop questions where a single synthesized, sourced answer is wanted."
- inputSchema (JSON Schema):
query(string, required) → maps to the requestinput.preset(string, optional, enum:fast|low|medium|high|xhigh, defaultmedium) → the research-depth preset.model(string, optional) → override the model; omit to let the preset decide.
- Behavior: POST to
/v1/agentwith{ input, preset, model? }and web search enabled (that's the point of the agent). Return the synthesized answer text plus a compact list of the cited sources (title + URL) as MCP text content.
- Request:
POST /v1/agent, headersAuthorization: Bearer <key>andContent-Type: application/json. Body at minimum{"model": "<id>", "input": "<query>"}; add the preset and enable the built-inweb_searchtool per the current docs. Built-in tools available includeweb_search,fetch_url,finance_search,people_search. - Response: a typed
outputarray plus anoutput_textconvenience field and citations. Extractoutput_textfor the answer; extract the citation/source list for the sources block. Handle the case whereoutput_textis absent by falling back to concatenating the text items inoutput. - Model IDs drift. Prefer relying on the
presetand leavingmodelunset by default. If a default model must be named, fetch a current valid ID fromGET /v1/modelsrather than hardcoding a guess.
Transport is newline-delimited JSON over stdin/stdout (one JSON-RPC message per line, UTF-8, no embedded newlines). Implement exactly these methods:
initialize→ respond with the negotiatedprotocolVersion(echo the client's requested version if you support it, else your latest),capabilities: { "tools": {} }, andserverInfo(name + version).notifications/initialized→ no-op (it's a notification; send no response).tools/list→ return{ "tools": [ <the tool schema above> ] }.tools/call→ params{ name, arguments }. Run the query; return{ "content": [ { "type": "text", "text": "<answer + sources>" } ], "isError": <bool> }.ping→ respond with an empty result.- Unknown method → return a proper JSON-RPC error object (code
-32601). - Notifications (no
id) get no response. Requests (withid) always get one response.
Reference: MCP spec at https://modelcontextprotocol.io (schema/basic lifecycle). Match the current protocol revision the doc lists.
- Agent API quickstart: https://docs.perplexity.ai/docs/agent-api/quickstart
- Create Agent Response (exact request/response schema — WebFetch this and confirm field names): https://docs.perplexity.ai/api-reference/agent-post
- OpenAI compatibility (why
/v1/agent, not chat.completions): https://docs.perplexity.ai/docs/agent-api/openai-compatibility - Agent API models + presets: https://docs.perplexity.ai/docs/agent-api/models
- Built-in tools (web_search etc.): https://docs.perplexity.ai/docs/agent-api/tools/web-search
- List models endpoint:
GET https://api.perplexity.ai/v1/models - MCP spec: https://modelcontextprotocol.io
PERPLEXITY_API_KEY=… python3 perplexity_agent_mcp.pystarts and blocks on stdin.- The self-test recipe drives
initialize→notifications/initialized→tools/list→tools/calland shows: a valid initialize result, theperplexity_agenttool in the list, and atools/callthat returns real answer text + at least one source URL. - Missing/empty
PERPLEXITY_API_KEY→ clean tool error, no traceback, no secret leak. - A non-2xx from Perplexity (e.g. bad model id) → readable error content, server stays alive.
grep-ing the file shows no third-party imports and the key is never written to stdout or any log.- Dropped into the client config below, the tool appears and answers a live query.
{
"mcpServers": {
"perplexity-agent": {
"command": "python3",
"args": ["/ABSOLUTE/PATH/TO/perplexity_agent_mcp.py"],
"env": { "PERPLEXITY_API_KEY": "pplx-…" }
}
}
}- Language: Python stdlib chosen for the cleanest zero-dependency single file. A Node
built-ins-only version (
https+readline, nonpm install) is equally valid if preferred — say so and it's a straight swap. - Tool surface: one
perplexity_agenttool proposed to stay lean. Easy follow-ons if wanted: afinance_search/people_search-focused variant, or exposingrecencyandsearch_domain_filter. - Billing: the Agent API is pay-as-you-go on the same Perplexity API account/key as the existing Sonar MCP; ensure the account has credits. Independent of the (cancelled) Pro sub.