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 and Sonar 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. |
- 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": "…" }
]
}A grounded answer from Sonar (OpenAI-compatible chat completion).
| Param | Type | Default | Notes |
|---|---|---|---|
question |
string | — (required) | 1–4096 chars |
model |
string | "sonar" |
"sonar" or "sonar-pro" only |
system_prompt |
string | null |
optional, ≤ 4096 chars |
{
"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 |
{ "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": [] }
}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>
All optional knobs are environment variables (see .env.example):
timeouts, response-size cap, retry count, rate limits, and an optional JSON
audit-log path.
uv sync --extra dev
uv run pytest # unit tests (no live API needed; httpx is mocked)
uv run ruff check . # lint
uv run pip-audit # dependency vulnerability scanMIT — see LICENSE.