#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Configuration
const SAP_AI_PROXY_URL = process.env.SAP_AI_PROXY_URL || "http://127.0.0.1:3030";
const DEFAULT_MODEL = process.env.PERPLEXITY_DEFAULT_MODEL || "sonar-pro";
// Types
interface PerplexityMessage {
role: "system" | "user" | "assistant";
content: string;
}
interface PerplexityResponse {
id: string;
model: string;
choices: Array<{
index: number;
message: {
role: string;
content: string;
};
finish_reason: string;
}>;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
citations?: string[];
search_results?: Array<{
title: string;
url: string;
snippet: string;
}>;
error?: string;
type?: string;
}
/**
* Call Perplexity API via SAP AI Core proxy
*/
async function callPerplexity(options: {
query: string;
model?: string;
search_recency_filter?: string;
return_citations?: boolean;
max_tokens?: number;
}): Promise<PerplexityResponse> {
const {
query,
model = DEFAULT_MODEL,
search_recency_filter,
return_citations = true,
max_tokens = 4096,
} = options;
const messages: PerplexityMessage[] = [
{
role: "user",
content: query,
},
];
const requestBody: Record<string, unknown> = {
model,
messages,
max_tokens,
};
// Add optional Perplexity-specific parameters
if (search_recency_filter) {
requestBody.search_recency_filter = search_recency_filter;
}
if (return_citations !== undefined) {
requestBody.return_citations = return_citations;
}
const response = await fetch(`${SAP_AI_PROXY_URL}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
const data = (await response.json()) as PerplexityResponse;
if (!response.ok) {
throw new Error(
(data as { error?: string }).error ||
`API request failed with status ${response.status}`
);
}
return data;
}
/**
* Format response with citations and search results
*/
function formatResponse(response: PerplexityResponse): string {
const content =
response.choices?.[0]?.message?.content || "No response content";
let result = content;
// Add citations if available
if (response.citations && response.citations.length > 0) {
result += "\n\n---\n**Sources:**\n";
response.citations.forEach((citation, index) => {
result += `${index + 1}. ${citation}\n`;
});
}
// Add search results if available (more detailed than citations)
if (response.search_results && response.search_results.length > 0) {
result += "\n**Search Results:**\n";
response.search_results.forEach((sr, index) => {
result += `${index + 1}. [${sr.title}](${sr.url})\n ${sr.snippet}\n`;
});
}
return result;
}
// Create MCP Server
const server = new McpServer({
name: "perplexity-search",
version: "1.0.0",
});
// Register perplexity_web_search tool (full-featured)
server.tool(
"perplexity_web_search",
"Search the web using Perplexity AI with real-time information and citations. Use this for comprehensive web searches that require up-to-date information, research, and source citations.",
{
query: z.string().describe("The search query to send to Perplexity"),
model: z
.enum(["sonar", "sonar-pro", "sonar-reasoning"])
.optional()
.describe(
"Perplexity model to use. sonar: fast search, sonar-pro: enhanced reasoning, sonar-reasoning: deep analysis"
),
search_recency_filter: z
.enum(["day", "week", "month", "year"])
.optional()
.describe("Filter search results by recency"),
return_citations: z
.boolean()
.optional()
.default(true)
.describe("Whether to return source citations (default: true)"),
},
async ({ query, model, search_recency_filter, return_citations }) => {
try {
const response = await callPerplexity({
query,
model,
search_recency_filter,
return_citations,
});
return {
content: [
{
type: "text" as const,
text: formatResponse(response),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error performing Perplexity search: ${errorMessage}`,
},
],
isError: true,
};
}
}
);
// Register perplexity_quick_search tool (simplified)
server.tool(
"perplexity_quick_search",
"Quick web search using Perplexity AI. Use this for simple queries when you need fast, real-time information with minimal configuration.",
{
query: z.string().describe("The quick search query"),
},
async ({ query }) => {
try {
const response = await callPerplexity({
query,
model: DEFAULT_MODEL, // Use configured default (sonar-pro)
return_citations: true,
max_tokens: 2048,
});
return {
content: [
{
type: "text" as const,
text: formatResponse(response),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error performing quick search: ${errorMessage}`,
},
],
isError: true,
};
}
}
);
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Perplexity Search MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});