diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index 81bef56b4..006f6bccd 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -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. @@ -25,4 +26,5 @@ export const registerAllTools = (server: McpServer): void => { registerAllSpotifyTools(server); registerContactTeamTool(server); registerUpdateAccountInfoTool(server); + registerSearchWebTool(server); }; diff --git a/lib/mcp/tools/registerSearchWebTool.ts b/lib/mcp/tools/registerSearchWebTool.ts new file mode 100644 index 000000000..8de65d2c0 --- /dev/null +++ b/lib/mcp/tools/registerSearchWebTool.ts @@ -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; + +/** + * 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", + ); + } + }, + ); +} diff --git a/lib/perplexity/config.ts b/lib/perplexity/config.ts new file mode 100644 index 000000000..af4abebe4 --- /dev/null +++ b/lib/perplexity/config.ts @@ -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}`, + }; +} diff --git a/lib/perplexity/formatSearchResultsAsMarkdown.ts b/lib/perplexity/formatSearchResultsAsMarkdown.ts new file mode 100644 index 000000000..7b218f109 --- /dev/null +++ b/lib/perplexity/formatSearchResultsAsMarkdown.ts @@ -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; +} diff --git a/lib/perplexity/searchPerplexity.ts b/lib/perplexity/searchPerplexity.ts new file mode 100644 index 000000000..c7c702037 --- /dev/null +++ b/lib/perplexity/searchPerplexity.ts @@ -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 { + 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}`); + } +}