MCP Perplexity Search
by spences10
Verified
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import {
PROMPT_TEMPLATES,
type CustomPromptTemplate,
type PromptTemplate,
} from './prompt-templates.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const pkg = JSON.parse(
readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
);
const { name, version } = pkg;
const PERPLEXITY_API_KEY = process.env.PERPLEXITY_API_KEY;
if (!PERPLEXITY_API_KEY) {
throw new Error(
'PERPLEXITY_API_KEY environment variable is required',
);
}
interface PerplexityResponse {
choices: Array<{
message: {
role: string;
content: string;
};
finish_reason: string;
}>;
}
class PerplexityServer {
private server: Server;
constructor() {
this.server = new Server(
{ name, version },
{
capabilities: {
tools: {},
},
},
);
this.setupToolHandlers();
}
private setupToolHandlers() {
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [
{
name: 'chat_completion',
description:
'Generate chat completions using the Perplexity API',
inputSchema: {
type: 'object',
properties: {
messages: {
type: 'array',
items: {
type: 'object',
required: ['role', 'content'],
properties: {
role: {
type: 'string',
enum: ['system', 'user', 'assistant'],
},
content: {
type: 'string',
},
},
},
},
prompt_template: {
type: 'string',
enum: Object.keys(PROMPT_TEMPLATES),
description:
'Predefined prompt template to use for common use cases. Available templates:\n' +
Object.entries(PROMPT_TEMPLATES)
.map(
([key, value]) =>
`- ${key}: ${value.description}`,
)
.join('\n'),
},
custom_template: {
type: 'object',
description:
'Custom prompt template. If provided, overrides prompt_template.',
properties: {
system: {
type: 'string',
description:
"System message that sets the assistant's role and behavior",
},
format: {
type: 'string',
enum: ['text', 'markdown', 'json'],
description: 'Response format',
},
include_sources: {
type: 'boolean',
description:
'Whether to include source URLs in responses',
},
},
required: ['system'],
},
format: {
type: 'string',
enum: ['text', 'markdown', 'json'],
description:
'Response format. Use json for structured data, markdown for formatted text with code blocks. Overrides template format if provided.',
default: 'text',
},
include_sources: {
type: 'boolean',
description:
'Include source URLs in the response. Overrides template setting if provided.',
default: false,
},
model: {
type: 'string',
enum: [
'sonar-pro',
'sonar',
'llama-3.1-sonar-small-128k-online',
'llama-3.1-sonar-large-128k-online',
'llama-3.1-sonar-huge-128k-online',
],
description:
'Model to use for completion. Note: llama-3.1 models will be deprecated after 2/22/2025',
default: 'sonar',
},
temperature: {
type: 'number',
minimum: 0,
maximum: 1,
default: 0.7,
description:
'Controls randomness in the output. Higher values (e.g. 0.8) make the output more random, while lower values (e.g. 0.2) make it more focused and deterministic.',
},
max_tokens: {
type: 'number',
minimum: 1,
maximum: 4096,
default: 1024,
description:
'The maximum number of tokens to generate in the response. One token is roughly 4 characters for English text.',
},
},
required: ['messages'],
},
},
],
}),
);
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
if (request.params.name !== 'chat_completion') {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`,
);
}
const {
messages,
prompt_template,
custom_template,
model = 'sonar',
temperature = 0.7,
max_tokens = 1024,
format: user_format,
include_sources: user_include_sources,
} = request.params.arguments as {
messages: Array<{ role: string; content: string }>;
prompt_template?: PromptTemplate;
custom_template?: CustomPromptTemplate;
model?: string;
temperature?: number;
max_tokens?: number;
format?: 'text' | 'markdown' | 'json';
include_sources?: boolean;
};
// Apply template if provided (custom template takes precedence)
const template =
custom_template ??
(prompt_template
? PROMPT_TEMPLATES[prompt_template]
: null);
const format = user_format ?? template?.format ?? 'text';
const include_sources =
user_include_sources ?? template?.include_sources ?? false;
// Merge template system message with user messages if template is provided
const final_messages = template
? [
{ role: 'system', content: template.system },
...messages,
]
: messages;
try {
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
30000,
); // 30 second timeout
try {
const response = await fetch(
'https://api.perplexity.ai/chat/completions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${PERPLEXITY_API_KEY}`,
},
body: JSON.stringify({
model,
messages: final_messages,
temperature,
max_tokens,
format,
include_sources,
}),
signal: controller.signal,
},
);
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({}));
throw new McpError(
ErrorCode.InternalError,
`Perplexity API error: ${response.statusText}${
errorData.error ? ` - ${errorData.error}` : ''
}`,
);
}
const data: PerplexityResponse = await response.json();
if (!data.choices?.[0]?.message) {
throw new McpError(
ErrorCode.InternalError,
'Invalid response format from Perplexity API',
);
}
return {
content: [
{
type: format === 'json' ? 'json' : 'text',
text: data.choices[0].message.content,
},
],
};
} finally {
clearTimeout(timeoutId);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error generating completion: ${
error instanceof Error
? error.message
: String(error)
}`,
},
],
isError: true,
};
}
},
);
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Perplexity MCP server running on stdio');
}
}
const server = new PerplexityServer();
server.run().catch(console.error);