Skip to content
Merged
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
2 changes: 2 additions & 0 deletions lib/mcp/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { registerAllSpotifyTools } from "./spotify";
import { registerContactTeamTool } from "./registerContactTeamTool";
import { registerUpdateAccountInfoTool } from "./registerUpdateAccountInfoTool";
import { registerAllArtistSocialsTools } from "./artistSocials";
import { registerSearchWebTool } from "./registerSearchWebTool";

/**
* Registers all MCP tools on the server.
Expand All @@ -25,4 +26,5 @@ export const registerAllTools = (server: McpServer): void => {
registerAllSpotifyTools(server);
registerContactTeamTool(server);
registerUpdateAccountInfoTool(server);
registerSearchWebTool(server);
};
65 changes: 65 additions & 0 deletions lib/mcp/tools/registerSearchWebTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { searchPerplexity } from "@/lib/perplexity/searchPerplexity";
import { formatSearchResultsAsMarkdown } from "@/lib/perplexity/formatSearchResultsAsMarkdown";
import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess";
import { getToolResultError } from "@/lib/mcp/getToolResultError";

const searchWebSchema = z.object({
query: z.string().min(1, "Search query is required").describe("The search query"),
max_results: z
.number()
.min(1)
.max(20)
.optional()
.describe("Maximum number of results (1-20, default: 10)"),
country: z
.string()
.length(2)
.optional()
.describe("ISO country code for regional results (e.g., 'US', 'GB')"),
});

type SearchWebArgs = z.infer<typeof searchWebSchema>;

/**
* Registers the "search_web" tool on the MCP server.
* Searches the web for real-time information using Perplexity API.
*
* @param server - The MCP server instance to register the tool on.
*/
export function registerSearchWebTool(server: McpServer): void {
server.registerTool(
"search_web",
{
description:
"DEFAULT TOOL: Use this tool FIRST for any information you're unsure about. " +
"NEVER respond with 'I can't find X', 'I don't have access to X', or 'I do not know X'. " +
"This tool searches the web for real-time information and should be your go-to resource. " +
"Returns ranked web search results with titles, URLs, and content snippets.",
inputSchema: searchWebSchema,
},
async (args: SearchWebArgs) => {
try {
const searchResponse = await searchPerplexity({
query: args.query,
max_results: args.max_results || 10,
max_tokens_per_page: 1024,
...(args.country && { country: args.country }),
});

const formattedResults = formatSearchResultsAsMarkdown(searchResponse);

return getToolResultSuccess({
results: searchResponse.results,
formatted: formattedResults,
});
} catch (error) {
console.error("Error searching the web:", error);
return getToolResultError(
error instanceof Error ? `Search failed: ${error.message}` : "Failed to search the web",
);
}
},
);
}
32 changes: 32 additions & 0 deletions lib/perplexity/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const PERPLEXITY_BASE_URL = "https://api.perplexity.ai";

/**
* Gets the Perplexity API key from the environment variables.
*
* @returns The Perplexity API key.
*/
export function getPerplexityApiKey(): string {
const apiKey = process.env.PERPLEXITY_API_KEY;

if (!apiKey) {
throw new Error(
"PERPLEXITY_API_KEY environment variable is not set. " +
"Please add it to your environment variables.",
);
}

return apiKey;
}

/**
* Gets the Perplexity headers for the API request.
*
* @param apiKey - The Perplexity API key.
* @returns The Perplexity headers.
*/
export function getPerplexityHeaders(apiKey: string): HeadersInit {
return {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
};
}
31 changes: 31 additions & 0 deletions lib/perplexity/formatSearchResultsAsMarkdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SearchResponse } from "./searchPerplexity";

/**
* Formats the search results as Markdown.
*
* @param response - The search response.
* @returns The formatted search results.
*/
export function formatSearchResultsAsMarkdown(response: SearchResponse): string {
const { results } = response;

if (results.length === 0) {
return "No search results found.";
}

let formatted = `Found ${results.length} search results:\n\n`;

results.forEach((result, index) => {
formatted += `### ${index + 1}. ${result.title}\n\n`;
formatted += `**URL:** ${result.url}\n\n`;

if (result.date) {
formatted += `**Published:** ${result.date}\n\n`;
}

formatted += `${result.snippet}\n\n`;
formatted += `---\n\n`;
});

return formatted;
}
60 changes: 60 additions & 0 deletions lib/perplexity/searchPerplexity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getPerplexityApiKey, getPerplexityHeaders, PERPLEXITY_BASE_URL } from "./config";

export interface SearchResult {
title: string;
url: string;
snippet: string;
date?: string;
last_updated?: string;
}

export interface SearchResponse {
results: SearchResult[];
id: string;
}

export interface SearchParams {
query: string;
max_results?: number;
max_tokens_per_page?: number;
country?: string;
search_domain_filter?: string[];
}

/**
* Searches the Perplexity API for the given query.
*
* @param params - The search parameters.
* @returns The search response.
*/
export async function searchPerplexity(params: SearchParams): Promise<SearchResponse> {
const apiKey = getPerplexityApiKey();
const url = `${PERPLEXITY_BASE_URL}/search`;

const body = {
query: params.query,
max_results: params.max_results || 10,
max_tokens_per_page: params.max_tokens_per_page || 1024,
...(params.country && { country: params.country }),
...(params.search_domain_filter && { search_domain_filter: params.search_domain_filter }),
};

try {
const response = await fetch(url, {
method: "POST",
headers: getPerplexityHeaders(apiKey),
body: JSON.stringify(body),
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Perplexity Search API error: ${response.status} ${response.statusText}\n${errorText}`,
);
}

return await response.json();
} catch (error) {
throw new Error(`Failed to search Perplexity API: ${error}`);
}
}