A simple, security-hardened Model Context Protocol (MCP) server that gives AI agents — Claude Code, Hermes, or any MCP client — access to the Perplexity Search, Sonar, and Responses-compatible Agent APIs.
It follows a retrieval-first reference architecture (search and synthesis kept
separate, citations validated against retrieval metadata) and applies the defensive
controls from the NSA's Model Context Protocol (MCP): Security Design Considerations
(May 2026). See SECURITY.md for the full control mapping.
- Canonical repo: https://codeberg.org/CryptoJones/PerplexityAgent
- Mirror: https://github.com/CryptoJones/PerplexityAgent
| Tool | Description |
|---|---|
perplexity_search |
Ranked web results from the Perplexity Search API. |
sonar_ask |
A grounded answer from Sonar / Sonar Pro (OpenAI-compatible chat). |
deep_research |
Multi-step pipeline: decompose → search each sub-question → dedupe → synthesize (JSON schema) → validate citations → return a cited report with a validation_report. |
responses_create |
Full Agent API: reasoning, multimodal input, built-in/custom tools, streaming events, and response chaining. |
responses_retrieve |
Retrieve a stored Agent response snapshot by its resp_ ID. |
finance_search |
Agent response with Perplexity's structured finance tool enabled. |
people_search |
Structured results from the Search API's people index. |
fetch_url |
Fetch a bounded set of public URLs through the SSRF-hardened page fetcher. |
registered_function_call |
Invoke an operator-registered Python handler; present only when a registry is configured. |
retrieve |
Retrieve a result offloaded because it exceeded the output budget. |
server_metrics |
In-process request, latency, and rate-limit counters. |
- Python ≥ 3.11
uv- A Perplexity API key (https://www.perplexity.ai/settings/api)
git clone https://codeberg.org/CryptoJones/PerplexityAgent.git
cd PerplexityAgent
uv sync # install (add --extra dev for tests)
cp .env.example .env # then edit .env and set PERPLEXITY_API_KEYThe API key is read server-side only (from the environment or .env) and is
never returned in any tool output. The server refuses to start without it.
Runs as a local subprocess of the agent with no network exposure:
uv run perplexity-agentclaude mcp add perplexity -- uv --directory /abs/path/to/PerplexityAgent run perplexity-agentor in your MCP client config (mcpServers):
{
"mcpServers": {
"perplexity": {
"command": "uv",
"args": ["--directory", "/abs/path/to/PerplexityAgent", "run", "perplexity-agent"],
"env": { "PERPLEXITY_API_KEY": "pplx-..." }
}
}
}Hermes consumes MCP servers over stdio the same way — point it at the
uv ... run perplexity-agent command with PERPLEXITY_API_KEY in the environment.
Off by default. It refuses to start without a bearer token and binds to
localhost. Only enable it if you understand the added attack surface (see
SECURITY.md):
PERPLEXITY_HTTP_AUTH_TOKEN="$(openssl rand -hex 32)" uv run perplexity-agent --transport httpClients must send Authorization: Bearer <token>. Terminate TLS in front of it
(reverse proxy) and keep it behind a filtering egress proxy.
Once the server is registered, the agent calls these tools automatically. The signatures, sample arguments, and return shapes are below.
Ranked web results from the Search API.
| Param | Type | Default | Bounds |
|---|---|---|---|
query |
string | — (required) | 1–4096 chars |
max_results |
int | 5 |
1–20 |
max_tokens_per_page |
int | 1024 |
128–4096 |
{ "query": "latest CRISPR base-editing clinical trials", "max_results": 8 }Returns the raw Search API payload, e.g.:
{
"results": [
{ "title": "…", "url": "https://…", "snippet": "…" }
]
}Optional Search API filters include search_domain_filter (up to 20 domains),
search_language_filter, search_recency_filter, and published/last-updated
before/after dates in M/D/YYYY form.
Calls POST /v1/agent using Perplexity's OpenAI Responses-compatible request
shape. It accepts text or message-array input, provider-qualified model (or a
fallback models list / preset), reasoning, response_format, tools,
max_output_tokens, and max_steps.
Set store: true, use responses_retrieve to read the returned ID later, and pass
that ID as previous_response_id to continue the response. Snapshots are kept in
the local SQLite store and scoped to the originating MCP session in addition to
Perplexity's upstream persistence. Retention defaults to the newest 100 snapshots
per session and is configurable with PERPLEXITY_MAX_RESPONSES_PER_SESSION.
Function tools support both client-driven and automatic chaining. Pass their JSON
schemas in tools; client-driven callers can submit a function_call_output with
the returned call_id. Server operators can instead pass an allowlisted
function_registry to build_server() and set auto_execute_functions: true.
Only registered handlers execute, arguments and outputs are bounded, errors become
tool outputs, and continuation rounds are capped. Registered handlers are also
available to MCP clients through registered_function_call.
For multimodal input, use message content parts:
{
"input": [{
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "Describe this image"},
{"type": "input_image", "image_url": "https://example.com/image.png"}
]
}],
"model": "openai/gpt-5.5"
}When stream: true, the API's SSE events (response.created, text deltas,
response.completed, and other item events) are parsed and returned in an
events array. MCP tool calls are request/response, so this surface collects the
events rather than emitting incremental MCP messages.
finance_searchtakes a natural-languagequeryplus optionalcategoriesandtickershints, and enables the Agent API'sfinance_searchtool.people_searchtakesquery,max_results, andmax_tokens_per_page, and routes the standard Search API throughsearch_type="people".fetch_urlaccepts eitherurlor aurlslist plus an explicitmax_urlscap (1–10), and returns extractedcontentswith final URLs, titles, text, and prompt-injection flags. It validates and pins every redirect hop to prevent SSRF and DNS rebinding.
Finance caching is disabled by default because prices change rapidly. Set
PERPLEXITY_FINANCE_CACHE_TTL_S to a small value (maximum 300 seconds) to enable
a bounded per-client cache for identical category/ticker queries.
A grounded answer from Sonar (OpenAI-compatible chat completion).
| Param | Type | Default | Notes |
|---|---|---|---|
question |
string | — (required) | 1–4096 chars |
model |
string | "sonar" |
legacy sonar/sonar-pro or an Agent provider/model ID |
system_prompt |
string | null |
optional, ≤ 4096 chars |
reasoning |
object | null |
optional Agent reasoning effort (low through max) |
response_format |
object | null |
optional text/JSON/JSON-schema output format |
max_output_tokens |
int | null |
required for Anthropic Agent models |
{
"question": "What changed in the EU AI Act's 2026 enforcement timeline?",
"model": "sonar-pro",
"system_prompt": "Answer concisely and cite sources."
}Returns the chat-completion payload; the answer is at
choices[0].message.content, with citations in the response metadata.
The full pipeline: decompose → search each sub-question → dedupe → synthesize (JSON schema) → validate citations.
| Param | Type | Default | Bounds |
|---|---|---|---|
question |
string | — (required) | 1–4096 chars |
num_subquestions |
int | 4 |
1–8 |
model |
string | "sonar-pro" |
"sonar" or "sonar-pro" |
max_results_per_subquestion |
int | 5 |
1–10 |
use_model_decomposition |
bool | false |
ask Sonar to derive the sub-questions (one extra call; falls back to the deterministic angles on any failure) |
{ "question": "Is small modular nuclear cost-competitive with grid-scale solar?", "num_subquestions": 5 }Returns a structured, citation-validated report:
{
"question": "…",
"subquestions": ["…", "…"],
"sources": [{ "title": "…", "url": "https://…", "snippet": "…" }],
"report": {
"answer": "…",
"key_findings": ["…"],
"open_questions": ["…"],
"claims": [
{ "claim": "…", "supporting_urls": ["https://…"], "confidence": "high" }
]
},
"validation_report": {
"total_claims": 6,
"all_claims_supported": true,
"all_urls_known": true,
"passed": true,
"flagged": []
},
"security_flags": { "possible_prompt_injection_patterns": [] },
"usage": { "prompt_tokens": 1234, "completion_tokens": 567 }
}A claim whose URL was never seen in retrieval is downgraded to low confidence
and listed in validation_report.flagged — passed is false if any claim is
unsupported or cites an unknown URL.
You don't construct the JSON yourself — just ask, and the model picks the tool:
> Use deep_research to assess whether small modular reactors are cost-competitive
with grid-scale solar, then summarize only the high-confidence claims.
Any MCP client works. Using the Python SDK that ships with this project:
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(
command="uv",
args=["--directory", "/abs/path/to/PerplexityAgent", "run", "perplexity-agent"],
env={"PERPLEXITY_API_KEY": "pplx-..."},
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
print([t.name for t in (await session.list_tools()).tools])
result = await session.call_tool(
"perplexity_search", {"query": "what is MCP", "max_results": 3}
)
print(result.content)
asyncio.run(main())You can also explore the tools interactively with the MCP Inspector:
npx @modelcontextprotocol/inspector uv run perplexity-agentWith the server started via --transport http, point any streamable-HTTP MCP
client at http://127.0.0.1:8080/mcp and send the bearer token:
Authorization: Bearer <PERPLEXITY_HTTP_AUTH_TOKEN>
An optional interactive terminal app that brings the spirit of Perplexity's Comet browser to the terminal, backed by the same Search / Sonar / deep-research client. A terminal can't render web pages, drive a real browser (clicking, booking, buying), or do voice — those are out of scope by physics. Everything else maps onto terminal-feasible equivalents:
| Comet feature | In the TUI |
|---|---|
| Assistant sidebar | A persistent chat pane (Sonar) that answers with your open "tabs" as context |
| Answer-first search | /search — ranked results plus a grounded, cited answer |
| Open / summarize a page | /open <url> — SSRF-guarded fetch → readable text → one-click summary |
| Ask about / translate a page | /ask <q>, /translate <lang> on the current page |
| Chat with your tabs / synthesis | /summary across all open tabs; bare chat is tab-aware |
| AI tab grouping | /group clusters open tabs into named groups |
| Deep research | /research <q> runs the full validated, cited pipeline |
| Memory & Spaces | Local SQLite store (owner-only 0600; tabs dedupe per space+URL, recent-tab cap and optional history retention are configurable); /space [name] switches workspaces |
| Background / scheduled tasks | /task search|fetch <seconds> <target> monitors and alerts on change; /untask <id> stops it |
| Agentic task planning | Research-only planning (decompose a goal); no real web actions |
Install the extra and launch it (needs PERPLEXITY_API_KEY, same as the server):
uv sync --extra tui
uv run perplexity-agent tuiThe page fetcher (/open) is the only egress path other than the Perplexity API and
is reachable only from the TUI, never via the MCP tools. It is SSRF-hardened
(scheme allowlist, private/loopback/link-local IPs rejected on every redirect hop,
connections pinned to the validated IP, a streaming size cap, and a text-only
content-type allowlist) and flags fetched text for indirect prompt injection before
it reaches Sonar. See SECURITY.md. The MCP tool surface is unchanged.
All optional knobs are environment variables (see .env.example):
timeouts, response-size cap, retry count, rate limits, an optional JSON audit-log
path, and (for the TUI) the fetch User-Agent, PERPLEXITY_FETCH_ALLOW_PRIVATE,
PERPLEXITY_STORE_PATH, and the per-Space store retention caps
(PERPLEXITY_MAX_TABS_PER_SPACE, default 50; PERPLEXITY_MAX_HISTORY_PER_SPACE,
default unbounded — set a positive integer to keep only the N most-recent rows
per Space, or leave blank to keep everything).
uv sync --extra dev --extra tui # add --extra tui to exercise the TUI tests
uv run pytest # unit tests (no live API needed; httpx is mocked)
uv run ruff check . # lint
uv run mypy src # type check (strict)
uv run pip-audit # dependency vulnerability scanMIT — see LICENSE.
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/