@jschuller/perplexity-mcp
Version:
MCP server for Perplexity AI — web search with recency filtering, citations, and model control for Claude Desktop and Claude Code
184 lines (183 loc) • 7.85 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import fetch from 'node-fetch';
// Get API key from environment
const apiKey = process.env.PERPLEXITY_API_KEY;
if (!apiKey) {
process.stderr.write('Error: PERPLEXITY_API_KEY environment variable is required\n');
process.exit(1);
}
// Default model to use
const defaultModel = process.env.PERPLEXITY_MODEL || 'sonar';
// Create the server instance
const server = new Server({
name: 'perplexity-mcp',
version: '2.1.1',
}, {
capabilities: {
tools: {},
},
});
// Define the perplexity_search_web tool
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'perplexity_search_web',
description: 'Search the web using Perplexity AI with recency filtering',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
recency: {
type: 'string',
enum: ['day', 'week', 'month', 'year'],
default: 'month',
description: 'Filter results by recency'
},
frequency_penalty: {
type: 'number',
description: 'Multiplicative penalty for new tokens based on their frequency in the text to avoid repetition. Mutually exclusive with the presence_penalty parameter.',
},
max_tokens: {
type: 'integer',
description: 'The maximum number of tokens to generate. Sum of max_tokens and prompt tokens should not exceed the model\'s context window limit.',
},
model: {
type: 'string',
description: 'The name of the model to use for generating completions. Options include sonar, sonar-pro, and other models listed at https://docs.perplexity.ai/guides/model-cards',
},
presence_penalty: {
type: 'number',
description: 'Penalty for new tokens based on their current presence in the text, encouraging topic variety. Mutually exclusive with the frequency_penalty parameter.',
},
return_citations: {
type: 'boolean',
description: 'Whether to include citations in the model\'s response.',
default: true,
},
return_images: {
type: 'boolean',
description: 'Whether to include images in the model\'s response.',
default: false,
},
stream: {
type: 'boolean',
description: 'Whether to stream the response incrementally using server-sent events.',
default: false,
},
temperature: {
type: 'number',
description: 'Controls generation randomness, with 0 being deterministic and values approaching 2 being more random.',
},
top_k: {
type: 'integer',
description: 'Limits the number of high-probability tokens to consider for generation. Set to 0 to disable.',
},
top_p: {
type: 'number',
description: 'Nucleus sampling threshold, controlling the token selection pool based on cumulative probability.',
}
},
required: ['query']
}
}
]
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'perplexity_search_web') {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const args = request.params.arguments;
if (!args || typeof args.query !== 'string') {
throw new Error('Query is required and must be a string');
}
const { query, recency = 'month', frequency_penalty, max_tokens, model = defaultModel, presence_penalty, return_citations = true, return_images = false, stream = false, temperature, top_k, top_p } = args;
// Construct system message with recency instruction
let systemContent = "You are a helpful assistant that searches the web to provide accurate, up-to-date information.";
if (recency) {
systemContent += ` Focus on results from the last ${recency}.`;
}
// Build the messages array
const messages = [
{
role: 'system',
content: systemContent
},
{
role: 'user',
content: query
}
];
// Build API request
const apiRequest = {
messages: messages,
model: model
};
// Add optional parameters if provided
if (frequency_penalty !== undefined)
apiRequest.frequency_penalty = frequency_penalty;
if (max_tokens !== undefined)
apiRequest.max_tokens = max_tokens;
if (presence_penalty !== undefined)
apiRequest.presence_penalty = presence_penalty;
if (return_citations !== undefined)
apiRequest.return_citations = return_citations;
if (return_images !== undefined)
apiRequest.return_images = return_images;
if (stream !== undefined)
apiRequest.stream = stream;
if (temperature !== undefined)
apiRequest.temperature = temperature;
if (top_k !== undefined)
apiRequest.top_k = top_k;
if (top_p !== undefined)
apiRequest.top_p = top_p;
try {
// Make API call to Perplexity
const response = await fetch('https://api.perplexity.ai/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(apiRequest)
});
if (!response.ok) {
const errorData = await response.text();
throw new Error(`Perplexity API error: ${response.status} ${response.statusText} - ${errorData}`);
}
const result = await response.json();
// Process and format the response
const formattedResponse = result.choices[0].message.content;
return {
content: [
{
type: 'text',
text: formattedResponse
}
],
meta: {
model: result.model || model,
usage: result.usage,
citations: result.choices[0].message.citations || [],
images: result.choices[0].message.images || []
}
};
}
catch (error) {
throw new Error(`Error in perplexity_search_web: ${error.message}`);
}
});
// Start the server
const transport = new StdioServerTransport();
server.connect(transport).catch((error) => {
process.stderr.write(`Server error: ${error}\n`);
process.exit(1);
});