Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions crates/perplexity-web-api-mcp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

mod server;

use perplexity_web_api::{AuthCookies, Client, ReasonModel, SearchModel};
use perplexity_web_api::{AuthCookies, Client, ComputerModel, ReasonModel, SearchModel};
use rmcp::{ServiceExt, transport::stdio};
use std::{env, env::VarError};
use tracing_subscriber::fmt;
Expand Down Expand Up @@ -110,32 +110,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tokenless = session_token.is_none() || csrf_token.is_none();
let incognito = optional_bool_env("PERPLEXITY_INCOGNITO", true)?;

let (default_ask_model, default_reason_model) = if tokenless {
let (default_ask_model, default_reason_model, default_computer_model) = if tokenless {
// In tokenless mode, model overrides are not supported.
if env::var("PERPLEXITY_ASK_MODEL").is_ok() {
return Err(std::io::Error::other(
"PERPLEXITY_ASK_MODEL cannot be used without authentication tokens.\n\n\
To use model configuration, provide both:\n\
PERPLEXITY_SESSION_TOKEN - Perplexity session token\n\
PERPLEXITY_CSRF_TOKEN - Perplexity CSRF token",
)
.into());
}
if env::var("PERPLEXITY_REASON_MODEL").is_ok() {
return Err(std::io::Error::other(
"PERPLEXITY_REASON_MODEL cannot be used without authentication tokens.\n\n\
To use model configuration, provide both:\n\
PERPLEXITY_SESSION_TOKEN - Perplexity session token\n\
PERPLEXITY_CSRF_TOKEN - Perplexity CSRF token",
)
.into());
// Use the same trim-and-check-empty semantics as optional_env/optional_model_env
// so that setting an empty/whitespace-only value is treated as "unset".
for name in [
"PERPLEXITY_ASK_MODEL",
"PERPLEXITY_REASON_MODEL",
"PERPLEXITY_COMPUTER_MODEL",
] {
if optional_env(name)?.is_some() {
return Err(std::io::Error::other(format!(
"{name} cannot be used without authentication tokens.\n\n\
To use model configuration, provide both:\n\
PERPLEXITY_SESSION_TOKEN - Perplexity session token\n\
PERPLEXITY_CSRF_TOKEN - Perplexity CSRF token",
))
.into());
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
(Some(SearchModel::Turbo), None)
(Some(SearchModel::Turbo), None, None)
} else {
let ask = optional_model_env::<SearchModel>("PERPLEXITY_ASK_MODEL")?
.unwrap_or(SearchModel::ProAuto);
let reason = optional_model_env::<ReasonModel>("PERPLEXITY_REASON_MODEL")?;
(Some(ask), reason)
let computer = optional_model_env::<ComputerModel>("PERPLEXITY_COMPUTER_MODEL")?;
(Some(ask), reason, computer)
};

if tokenless {
Expand Down Expand Up @@ -167,6 +167,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
client,
default_ask_model,
default_reason_model,
default_computer_model,
tokenless,
incognito,
);
Expand Down
119 changes: 115 additions & 4 deletions crates/perplexity-web-api-mcp/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use base64::Engine as _;
use perplexity_web_api::{
Client, ModelPreference, ReasonModel, SearchMode, SearchModel, SearchRequest,
Client, ComputerModel, ModelPreference, ReasonModel, SearchMode, SearchModel, SearchRequest,
SearchWebResult, Source, UploadFile,
};
use rmcp::{
Expand Down Expand Up @@ -35,7 +35,12 @@ pub struct PerplexitySearchRequest {
/// The search query or question to ask.
pub query: String,

/// Information sources to search. Valid values: "web", "scholar", "social".
/// Information sources to search.
/// Standard (no auth): "web", "scholar", "social".
/// Connectors (require auth + connected account): "google_drive", "gcal",
/// "outlook", "notion_mcp", "github_mcp_direct", "linear_alt",
/// "slack_direct", "jira_mcp_merge", "confluence_mcp_merge",
/// "microsoft_teams_mcp_merge", "onedrive", "sharepoint", "dropbox", "box".
/// Defaults to ["web"] if not specified.
#[serde(default)]
pub sources: Option<Vec<String>>,
Expand All @@ -51,7 +56,12 @@ pub struct PerplexityRequest {
/// The search query or question to ask.
pub query: String,

/// Information sources to search. Valid values: "web", "scholar", "social".
/// Information sources to search.
/// Standard (no auth): "web", "scholar", "social".
/// Connectors (require auth + connected account): "google_drive", "gcal",
/// "outlook", "notion_mcp", "github_mcp_direct", "linear_alt",
/// "slack_direct", "jira_mcp_merge", "confluence_mcp_merge",
/// "microsoft_teams_mcp_merge", "onedrive", "sharepoint", "dropbox", "box".
/// Defaults to ["web"] if not specified.
#[serde(default)]
pub sources: Option<Vec<String>>,
Expand Down Expand Up @@ -109,6 +119,7 @@ pub struct PerplexityServer {
client: Client,
ask_model: Option<SearchModel>,
reason_model: Option<ReasonModel>,
computer_model: Option<ComputerModel>,
tokenless: bool,
incognito: bool,
}
Expand All @@ -131,10 +142,11 @@ impl PerplexityServer {
client: Client,
ask_model: Option<SearchModel>,
reason_model: Option<ReasonModel>,
computer_model: Option<ComputerModel>,
tokenless: bool,
incognito: bool,
) -> Self {
Self { client, ask_model, reason_model, tokenless, incognito }
Self { client, ask_model, reason_model, computer_model, tokenless, incognito }
}

/// Converts a `FileAttachment` from tool parameters into an `UploadFile`.
Expand Down Expand Up @@ -387,6 +399,98 @@ impl PerplexityServer {
.await?,
)
}

/// Perplexity Computer, an agentic AI that can browse the web, run code, and use connected services.
///
/// Best for: Tasks that require the LLM to take actions, browse the web,
/// run code, or interact with connected services.
#[tool(
name = "perplexity_computer",
description = "Execute a task using Perplexity Computer, an agentic AI that can browse the web, \
run code, and use connected services. \
Best for: multi-step tasks, data analysis with connectors (Google Drive, Calendar, Notion, GitHub), \
web automation, and tasks requiring tool use. \
Requires authentication tokens. Significantly slower than other tools (30+ seconds). \
Sources can include connected services like google_drive, gcal, notion_mcp, github_mcp_direct, etc. \
Supports optional file attachments via the `files` parameter.",
annotations(
title = "Perplexity Computer",
read_only_hint = false,
open_world_hint = true,
destructive_hint = false,
idempotent_hint = false
)
)]
pub async fn perplexity_computer(
&self,
Parameters(params): Parameters<PerplexityRequest>,
) -> Result<CallToolResult, McpError> {
to_json_tool_result(
&self
.do_search(
params,
SearchMode::Computer,
self.computer_model.map(ModelPreference::from),
true,
)
.await?,
)
}

/// Study mode — tutor-style explanations with step-by-step teaching.
///
/// Best for: learning new concepts, guided explanations, and educational
/// breakdowns of complex topics.
#[tool(
name = "perplexity_study",
description = "Answer a question in tutor-style study mode with step-by-step, \
pedagogical explanations. \
Best for: learning new concepts, guided walkthroughs, test prep, \
and educational breakdowns. \
Requires authentication tokens. \
Supports optional file attachments via the `files` parameter.",
annotations(
title = "Study Mode",
read_only_hint = true,
open_world_hint = true,
destructive_hint = false,
idempotent_hint = false
)
)]
pub async fn perplexity_study(
&self,
Parameters(params): Parameters<PerplexityRequest>,
) -> Result<CallToolResult, McpError> {
to_json_tool_result(&self.do_search(params, SearchMode::Study, None, true).await?)
}

/// Document review mode — detailed analysis of uploaded documents.
///
/// Best for: extracting findings, summarizing long documents, and reviewing
/// contracts, papers, or reports with uploaded files.
#[tool(
name = "perplexity_document_review",
description = "Perform detailed analysis and review of uploaded documents. \
Best for: contract review, paper analysis, long-document summarization, \
and extracting structured findings from PDFs or text files. \
Requires authentication tokens. \
Attach the document(s) via the `files` parameter (required for best results).",
annotations(
title = "Document Review",
read_only_hint = true,
open_world_hint = true,
destructive_hint = false,
idempotent_hint = false
)
)]
pub async fn perplexity_document_review(
&self,
Parameters(params): Parameters<PerplexityRequest>,
) -> Result<CallToolResult, McpError> {
to_json_tool_result(
&self.do_search(params, SearchMode::DocumentReview, None, true).await?,
)
}
}

#[tool_handler]
Expand All @@ -402,6 +506,13 @@ impl ServerHandler for PerplexityServer {
instructions.push_str(
" Use perplexity_research for in-depth multi-source investigation (slow, 60s+). \
Use perplexity_reason for complex analysis requiring step-by-step logic. \
Use perplexity_computer for agentic tasks with tool use and connected services. \
Use perplexity_study for tutor-style step-by-step explanations. \
Use perplexity_document_review for detailed analysis of uploaded documents. \
All tools accept a `sources` parameter for connector-scoped queries: \
standard sources (web, scholar, social) work without auth; \
connectors (google_drive, gcal, outlook, notion_mcp, github_mcp_direct, etc.) \
require authentication and a connected account. \
All tools support an optional `files` parameter for document analysis: \
pass an array of objects each with `filename` and either `text` (plain-text content) \
or `data` (base64-encoded binary content, e.g. for PDFs).",
Expand Down
19 changes: 14 additions & 5 deletions crates/perplexity-web-api/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,18 +178,21 @@ impl Client {

let mode_str = match request.mode {
SearchMode::Auto => API_MODE_CONCISE,
SearchMode::Pro | SearchMode::Reasoning | SearchMode::DeepResearch => {
API_MODE_COPILOT
}
SearchMode::Pro
| SearchMode::Reasoning
| SearchMode::DeepResearch
| SearchMode::Computer
| SearchMode::Study
| SearchMode::DocumentReview => API_MODE_COPILOT,
};

let model_pref = request
.model_preference
.map(|preference| preference.as_str())
.unwrap_or_else(|| request.mode.default_preference());

let sources_str: Vec<&'static str> =
request.sources.iter().map(|s| s.as_str()).collect();
let sources_str: Vec<String> =
request.sources.iter().map(|s| s.as_str().to_owned()).collect();

let payload = AskPayload {
query_str: &request.query,
Expand All @@ -204,6 +207,7 @@ impl Client {
model_preference: model_pref,
source: "default",
sources: sources_str,
query_source: request.mode.query_source(),
version: API_VERSION,
},
};
Expand Down Expand Up @@ -244,6 +248,11 @@ impl Client {
return Err(Error::FileUploadRequiresAuth);
}

let needs_auth = request.sources.iter().any(|s| !s.is_public());
if needs_auth && !self.has_cookies {
return Err(Error::ConnectorRequiresAuth);
}

Ok(())
}
}
4 changes: 4 additions & 0 deletions crates/perplexity-web-api/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub enum Error {
#[error("File uploads require authentication cookies")]
FileUploadRequiresAuth,

/// Connector sources require authentication cookies.
#[error("Connector sources (e.g. google_drive, gcal, notion_mcp) require authentication cookies")]
ConnectorRequiresAuth,

/// Failed to get upload URL.
#[error("Failed to get upload URL: {0}")]
UploadUrlFailed(#[source] rquest::Error),
Expand Down
10 changes: 9 additions & 1 deletion crates/perplexity-web-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,20 @@
//! - [`SearchMode::Pro`] - Enhanced mode with access to premium models
//! - [`SearchMode::Reasoning`] - Chain-of-thought reasoning models
//! - [`SearchMode::DeepResearch`] - Extended research capabilities
//! - [`SearchMode::Computer`] - Perplexity Computer agentic execution
//!
//! # Sources
//!
//! Standard (no auth required):
//! - [`Source::Web`] - General web search (default)
//! - [`Source::Scholar`] - Academic papers and research
//! - [`Source::Social`] - Social media content
//!
//! Connectors (require authentication and connected accounts):
//! - [`Source::GoogleDrive`], [`Source::GoogleCalendar`], [`Source::Outlook`]
//! - [`Source::Notion`], [`Source::GitHub`], [`Source::Slack`], [`Source::Linear`]
//! - [`Source::Jira`], [`Source::Confluence`], [`Source::MicrosoftTeams`]
//! - [`Source::Custom`] - User-specific remote MCP connectors

mod auth;
mod client;
Expand All @@ -93,7 +101,7 @@ mod upload;
pub use auth::{AuthCookies, CSRF_TOKEN_COOKIE_NAME, SESSION_TOKEN_COOKIE_NAME};
pub use client::{Client, ClientBuilder};
pub use error::{Error, Result};
pub use models::{ModelPreference, ReasonModel, SearchModel};
pub use models::{ComputerModel, ModelPreference, ReasonModel, SearchModel};
pub use types::{
FollowUpContext, SearchEvent, SearchMode, SearchRequest, SearchResponse, SearchWebResult,
Source, UploadFile,
Expand Down
Loading