diff --git a/README.md b/README.md index b2c854c..1d322b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Perplexity Ask MCP Server -An MCP server implementation that integrates the Sonar API to provide Claude with unparalleled real-time, web-wide research. +An MCP server implementation that integrates the Sonar API to provide Claude with unparalleled real-time, web-wide research capabilities, now enhanced with advanced date filtering and domain filtering options. Please refer to the official [DeepWiki page](https://deepwiki.com/ppl-ai/modelcontextprotocol) for assistance with implementation. @@ -10,22 +10,151 @@ Please refer to the official [DeepWiki page](https://deepwiki.com/ppl-ai/modelco ![System Architecture](perplexity-ask/assets/system_architecture.png) +![Demo](perplexity-ask/assets/demo_screenshot.png) +## Tools +### **perplexity_ask** +- Engage in a conversation with the Sonar API for live web searches. +- **Inputs:** + - `messages` (array): An array of conversation messages. + - Each message must include: + - `role` (string): The role of the message (e.g., `system`, `user`, `assistant`). + - `content` (string): The content of the message. + - **Date Filtering Options** (all optional): + - `search_after_date_filter` (string): Filter to content published after this date (format: M/D/YYYY) + - `search_before_date_filter` (string): Filter to content published before this date (format: M/D/YYYY) + - `last_updated_after_filter` (string): Filter to content updated after this date (format: M/D/YYYY) + - `last_updated_before_filter` (string): Filter to content updated before this date (format: M/D/YYYY) + - `search_recency_filter` (string): Filter by predefined periods ("day", "week", "month", "year") + - **Domain Filtering Options** (optional): + - `search_domain_filter` (array): Filter search results by domain or URL + - Allowlist mode: `["wikipedia.org", "github.com"]` - Include only these domains + - Denylist mode: `["-reddit.com", "-quora.com"]` - Exclude these domains + - Supports up to 20 entries, can mix domains and specific URLs + +### **perplexity_research** +- Perform deep research queries using the Perplexity API with comprehensive analysis. +- **Inputs:** Same as `perplexity_ask` including all date and domain filtering options. + +### **perplexity_reason** +- Execute advanced reasoning tasks with enhanced analytical capabilities. +- **Inputs:** Same as `perplexity_ask` including all date and domain filtering options. + +## Date Filtering Features + +### Publication Date Filters +Filter results based on when content was originally created or published: +- `search_after_date_filter`: Include only content published after the specified date +- `search_before_date_filter`: Include only content published before the specified date + +### Last Updated Date Filters +Filter results based on when content was last modified: +- `last_updated_after_filter`: Include only content updated after the specified date +- `last_updated_before_filter`: Include only content updated before the specified date + +### Recency Filter +Quick filtering by predefined time periods relative to the current date: +- `search_recency_filter`: Use "day", "week", "month", or "year" for convenience + +## Domain Filtering Features + +Control which websites are included or excluded from search results: + +### Allowlist Mode +Include only specified domains/URLs: +```json +"search_domain_filter": ["wikipedia.org", "github.com", "stackoverflow.com"] +``` +### Denylist Mode +Exclude specified domains/URLs (use `-` prefix): +```json +"search_domain_filter": ["-reddit.com", "-pinterest.com", "-quora.com"] +``` -![Demo](perplexity-ask/assets/demo_screenshot.png) +### URL-Level Filtering +Target specific pages for granular control: +```json +"search_domain_filter": [ + "https://docs.python.org/3/tutorial/", + "https://en.wikipedia.org/wiki/Machine_learning" +] +``` +### Filter Usage Examples -## Tools +1. **Find recent AI developments from the past week:** +```json +{ + "messages": [{"role": "user", "content": "What are the latest AI developments?"}], + "search_recency_filter": "week" +} +``` + +2. **Research articles published in March 2025:** +```json +{ + "messages": [{"role": "user", "content": "Machine learning research trends"}], + "search_after_date_filter": "3/1/2025", + "search_before_date_filter": "3/31/2025" +} +``` + +3. **Find recently updated documentation:** +```json +{ + "messages": [{"role": "user", "content": "React development best practices"}], + "last_updated_after_filter": "1/1/2025" +} +``` + +4. **Combine publication and update filters:** +```json +{ + "messages": [{"role": "user", "content": "Python tutorials"}], + "search_after_date_filter": "1/1/2024", + "search_before_date_filter": "12/31/2024", + "last_updated_after_filter": "2/1/2025" +} +``` + +5. **Search only trusted academic sources:** +```json +{ + "messages": [{"role": "user", "content": "AI research papers"}], + "search_domain_filter": ["arxiv.org", "nature.com", "science.org"] +} +``` + +6. **Exclude social media and forums:** +```json +{ + "messages": [{"role": "user", "content": "Programming tutorials"}], + "search_domain_filter": ["-reddit.com", "-quora.com", "-pinterest.com"] +} +``` + +7. **Target specific documentation with recent updates:** +```json +{ + "messages": [{"role": "user", "content": "React hooks documentation"}], + "search_domain_filter": ["https://react.dev/docs/hooks", "github.com/facebook/react"], + "last_updated_after_filter": "1/1/2025" +} +``` -- **perplexity_ask** - - Engage in a conversation with the Sonar API for live web searches. - - **Inputs:** - - `messages` (array): An array of conversation messages. - - Each message must include: - - `role` (string): The role of the message (e.g., `system`, `user`, `assistant`). - - `content` (string): The content of the message. +### Important Notes +- **Date filters:** + - Date format must be exactly M/D/YYYY (e.g., "3/1/2025" or "03/01/2025") + - `search_recency_filter` cannot be combined with other date filters + - All date filters are optional and can be used independently or together (except recency filter) +- **Domain filters:** + - Maximum 20 domains/URLs per filter + - Cannot mix allowlist and denylist modes in the same request + - Use simple domain names (`example.com`) for broad filtering + - Use complete URLs (`https://example.com/page`) for specific page targeting + - Domain filtering applies to all subdomains (e.g., `nytimes.com` includes all subdomains) ## Configuration diff --git a/perplexity-ask/index.ts b/perplexity-ask/index.ts index f5e5d6d..b1b99b7 100644 --- a/perplexity-ask/index.ts +++ b/perplexity-ask/index.ts @@ -1,5 +1,74 @@ #!/usr/bin/env node +/** + * Perplexity MCP Server with Date Filtering Support + * + * This server provides three tools for interacting with the Perplexity API: + * - perplexity_ask: Basic conversation tool + * - perplexity_research: Deep research queries + * - perplexity_reason: Advanced reasoning tasks + * + * All tools support optional date filtering parameters: + * + * PUBLICATION DATE FILTERS: + * - search_after_date_filter: Include only content published after this date (format: M/D/YYYY) + * - search_before_date_filter: Include only content published before this date (format: M/D/YYYY) + * + * LAST UPDATED FILTERS: + * - last_updated_after_filter: Include only content updated after this date (format: M/D/YYYY) + * - last_updated_before_filter: Include only content updated before this date (format: M/D/YYYY) + * + * RECENCY FILTER: + * - search_recency_filter: Quick filter by predefined periods ("day", "week", "month", "year") + * Note: Cannot be combined with other date filters + * + * DOMAIN FILTERING: + * - search_domain_filter: Array of domains/URLs to include (allowlist) or exclude (denylist) + * - Allowlist mode: ["domain1.com", "domain2.com"] - Include only these domains + * - Denylist mode: ["-domain1.com", "-domain2.com"] - Exclude these domains + * - URL-level filtering: ["https://example.com/specific-page"] for granular control + * - Maximum 20 domains/URLs allowed + * + * EXAMPLES: + * + * 1. Find recent AI news from the past week: + * { + * "messages": [{"role": "user", "content": "What's new in AI?"}], + * "search_recency_filter": "week" + * } + * + * 2. Research articles published in March 2025: + * { + * "messages": [{"role": "user", "content": "Machine learning trends"}], + * "search_after_date_filter": "3/1/2025", + * "search_before_date_filter": "3/31/2025" + * } + * + * 3. Find recently updated documentation: + * { + * "messages": [{"role": "user", "content": "React best practices"}], + * "last_updated_after_filter": "1/1/2025" + * } + * + * 4. Search only trusted domains: + * { + * "messages": [{"role": "user", "content": "AI research papers"}], + * "search_domain_filter": ["arxiv.org", "nature.com", "science.org"] + * } + * + * 5. Exclude social media and forum sites: + * { + * "messages": [{"role": "user", "content": "Machine learning tutorials"}], + * "search_domain_filter": ["-reddit.com", "-quora.com", "-pinterest.com"] + * } + * + * 6. Target specific documentation pages: + * { + * "messages": [{"role": "user", "content": "Python async programming"}], + * "search_domain_filter": ["https://docs.python.org/3/library/asyncio.html", "stackoverflow.com"] + * } + */ + import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { @@ -12,13 +81,16 @@ import { * Definition of the Perplexity Ask Tool. * This tool accepts an array of messages and returns a chat completion response * from the Perplexity API, with citations appended to the message if provided. + * Supports date and time filtering options. */ const PERPLEXITY_ASK_TOOL: Tool = { name: "perplexity_ask", description: "Engages in a conversation using the Sonar API. " + "Accepts an array of messages (each with a role and content) " + - "and returns a ask completion response from the Perplexity model.", + "and returns a ask completion response from the Perplexity model. " + + "Supports optional date filtering to constrain search results by publication date, last updated date, or recency, " + + "and domain filtering to include or exclude specific websites from search results.", inputSchema: { type: "object", properties: { @@ -40,6 +112,34 @@ const PERPLEXITY_ASK_TOOL: Tool = { }, description: "Array of conversation messages", }, + search_after_date_filter: { + type: "string", + description: "Filter results to content published after this date. Format: M/D/YYYY (e.g., '3/1/2025')", + }, + search_before_date_filter: { + type: "string", + description: "Filter results to content published before this date. Format: M/D/YYYY (e.g., '3/5/2025')", + }, + last_updated_after_filter: { + type: "string", + description: "Filter results to content last updated after this date. Format: M/D/YYYY (e.g., '3/1/2025')", + }, + last_updated_before_filter: { + type: "string", + description: "Filter results to content last updated before this date. Format: M/D/YYYY (e.g., '3/5/2025')", + }, + search_recency_filter: { + type: "string", + enum: ["day", "week", "month", "year"], + description: "Filter results by predefined time periods relative to current date. Cannot be combined with other date filters.", + }, + search_domain_filter: { + type: "array", + items: { + type: "string", + }, + description: "Filter search results by domain or URL. Use allowlist mode (include only specified domains/URLs) or denylist mode (exclude domains/URLs with '-' prefix). Examples: ['wikipedia.org', 'github.com'] for allowlist, ['-reddit.com', '-quora.com'] for denylist. Supports up to 20 entries. Can mix domains ('example.com') and specific URLs ('https://example.com/page').", + }, }, required: ["messages"], }, @@ -48,13 +148,16 @@ const PERPLEXITY_ASK_TOOL: Tool = { /** * Definition of the Perplexity Research Tool. * This tool performs deep research queries using the Perplexity API. + * Supports date and time filtering options. */ const PERPLEXITY_RESEARCH_TOOL: Tool = { name: "perplexity_research", description: "Performs deep research using the Perplexity API. " + "Accepts an array of messages (each with a role and content) " + - "and returns a comprehensive research response with citations.", + "and returns a comprehensive research response with citations. " + + "Supports optional date filtering to constrain search results by publication date, last updated date, or recency, " + + "and domain filtering to include or exclude specific websites from search results.", inputSchema: { type: "object", properties: { @@ -76,6 +179,34 @@ const PERPLEXITY_RESEARCH_TOOL: Tool = { }, description: "Array of conversation messages", }, + search_after_date_filter: { + type: "string", + description: "Filter results to content published after this date. Format: M/D/YYYY (e.g., '3/1/2025')", + }, + search_before_date_filter: { + type: "string", + description: "Filter results to content published before this date. Format: M/D/YYYY (e.g., '3/5/2025')", + }, + last_updated_after_filter: { + type: "string", + description: "Filter results to content last updated after this date. Format: M/D/YYYY (e.g., '3/1/2025')", + }, + last_updated_before_filter: { + type: "string", + description: "Filter results to content last updated before this date. Format: M/D/YYYY (e.g., '3/5/2025')", + }, + search_recency_filter: { + type: "string", + enum: ["day", "week", "month", "year"], + description: "Filter results by predefined time periods relative to current date. Cannot be combined with other date filters.", + }, + search_domain_filter: { + type: "array", + items: { + type: "string", + }, + description: "Filter search results by domain or URL. Use allowlist mode (include only specified domains/URLs) or denylist mode (exclude domains/URLs with '-' prefix). Examples: ['wikipedia.org', 'github.com'] for allowlist, ['-reddit.com', '-quora.com'] for denylist. Supports up to 20 entries. Can mix domains ('example.com') and specific URLs ('https://example.com/page').", + }, }, required: ["messages"], }, @@ -84,13 +215,16 @@ const PERPLEXITY_RESEARCH_TOOL: Tool = { /** * Definition of the Perplexity Reason Tool. * This tool performs reasoning queries using the Perplexity API. + * Supports date and time filtering options. */ const PERPLEXITY_REASON_TOOL: Tool = { name: "perplexity_reason", description: "Performs reasoning tasks using the Perplexity API. " + "Accepts an array of messages (each with a role and content) " + - "and returns a well-reasoned response using the sonar-reasoning-pro model.", + "and returns a well-reasoned response using the sonar-reasoning-pro model. " + + "Supports optional date filtering to constrain search results by publication date, last updated date, or recency, " + + "and domain filtering to include or exclude specific websites from search results.", inputSchema: { type: "object", properties: { @@ -112,6 +246,34 @@ const PERPLEXITY_REASON_TOOL: Tool = { }, description: "Array of conversation messages", }, + search_after_date_filter: { + type: "string", + description: "Filter results to content published after this date. Format: M/D/YYYY (e.g., '3/1/2025')", + }, + search_before_date_filter: { + type: "string", + description: "Filter results to content published before this date. Format: M/D/YYYY (e.g., '3/5/2025')", + }, + last_updated_after_filter: { + type: "string", + description: "Filter results to content last updated after this date. Format: M/D/YYYY (e.g., '3/1/2025')", + }, + last_updated_before_filter: { + type: "string", + description: "Filter results to content last updated before this date. Format: M/D/YYYY (e.g., '3/5/2025')", + }, + search_recency_filter: { + type: "string", + enum: ["day", "week", "month", "year"], + description: "Filter results by predefined time periods relative to current date. Cannot be combined with other date filters.", + }, + search_domain_filter: { + type: "array", + items: { + type: "string", + }, + description: "Filter search results by domain or URL. Use allowlist mode (include only specified domains/URLs) or denylist mode (exclude domains/URLs with '-' prefix). Examples: ['wikipedia.org', 'github.com'] for allowlist, ['-reddit.com', '-quora.com'] for denylist. Supports up to 20 entries. Can mix domains ('example.com') and specific URLs ('https://example.com/page').", + }, }, required: ["messages"], }, @@ -124,28 +286,105 @@ if (!PERPLEXITY_API_KEY) { process.exit(1); } +/** + * Interface for filtering options + */ +interface FilterOptions { + search_after_date_filter?: string; + search_before_date_filter?: string; + last_updated_after_filter?: string; + last_updated_before_filter?: string; + search_recency_filter?: "day" | "week" | "month" | "year"; + search_domain_filter?: string[]; +} + +/** + * Validates filter options to ensure they follow the correct format and constraints + */ +function validateFilters(filters: FilterOptions): void { + const dateRegex = /^(0?[1-9]|1[0-2])\/(0?[1-9]|[12][0-9]|3[01])\/[0-9]{4}$/; + + // Validate date format for specific date filters + if (filters.search_after_date_filter && !dateRegex.test(filters.search_after_date_filter)) { + throw new Error("search_after_date_filter must be in M/D/YYYY format (e.g., '3/1/2025')"); + } + if (filters.search_before_date_filter && !dateRegex.test(filters.search_before_date_filter)) { + throw new Error("search_before_date_filter must be in M/D/YYYY format (e.g., '3/5/2025')"); + } + if (filters.last_updated_after_filter && !dateRegex.test(filters.last_updated_after_filter)) { + throw new Error("last_updated_after_filter must be in M/D/YYYY format (e.g., '3/1/2025')"); + } + if (filters.last_updated_before_filter && !dateRegex.test(filters.last_updated_before_filter)) { + throw new Error("last_updated_before_filter must be in M/D/YYYY format (e.g., '3/5/2025')"); + } + + // Validate that search_recency_filter is not combined with other date filters + if (filters.search_recency_filter && + (filters.search_after_date_filter || filters.search_before_date_filter || + filters.last_updated_after_filter || filters.last_updated_before_filter)) { + throw new Error("search_recency_filter cannot be combined with other date filters"); + } + + // Validate domain filter constraints + if (filters.search_domain_filter) { + if (filters.search_domain_filter.length > 20) { + throw new Error("search_domain_filter can contain a maximum of 20 domains/URLs"); + } + + // Check if it's mixing allowlist and denylist modes + const hasAllowlist = filters.search_domain_filter.some(domain => !domain.startsWith('-')); + const hasDenylist = filters.search_domain_filter.some(domain => domain.startsWith('-')); + + if (hasAllowlist && hasDenylist) { + throw new Error("search_domain_filter cannot mix allowlist and denylist modes. Use either domains without '-' prefix (allowlist) or domains with '-' prefix (denylist), but not both."); + } + } +} + /** * Performs a chat completion by sending a request to the Perplexity API. * Appends citations to the returned message content if they exist. * * @param {Array<{ role: string; content: string }>} messages - An array of message objects. * @param {string} model - The model to use for the completion. + * @param {FilterOptions} filters - Optional filtering parameters for dates and domains. * @returns {Promise} The chat completion result with appended citations. * @throws Will throw an error if the API request fails. */ async function performChatCompletion( messages: Array<{ role: string; content: string }>, - model: string = "sonar-pro" + model: string = "sonar-pro", + filters: FilterOptions = {} ): Promise { + // Validate filters + validateFilters(filters); + // Construct the API endpoint URL and request body const url = new URL("https://api.perplexity.ai/chat/completions"); - const body = { + const body: any = { model: model, // Model identifier passed as parameter messages: messages, - // Additional parameters can be added here if required (e.g., max_tokens, temperature, etc.) - // See the Sonar API documentation for more details: - // https://docs.perplexity.ai/api-reference/chat-completions }; + + // Add filter parameters if provided + if (filters.search_after_date_filter) { + body.search_after_date_filter = filters.search_after_date_filter; + } + if (filters.search_before_date_filter) { + body.search_before_date_filter = filters.search_before_date_filter; + } + if (filters.last_updated_after_filter) { + body.last_updated_after_filter = filters.last_updated_after_filter; + } + if (filters.last_updated_before_filter) { + body.last_updated_before_filter = filters.last_updated_before_filter; + } + if (filters.search_recency_filter) { + body.search_recency_filter = filters.search_recency_filter; + } + if (filters.search_domain_filter && filters.search_domain_filter.length > 0) { + body.search_domain_filter = filters.search_domain_filter; + } let response; try { @@ -230,14 +469,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!args) { throw new Error("No arguments provided"); } + + // Extract filter parameters from arguments + const filters: FilterOptions = { + search_after_date_filter: args.search_after_date_filter as string | undefined, + search_before_date_filter: args.search_before_date_filter as string | undefined, + last_updated_after_filter: args.last_updated_after_filter as string | undefined, + last_updated_before_filter: args.last_updated_before_filter as string | undefined, + search_recency_filter: args.search_recency_filter as "day" | "week" | "month" | "year" | undefined, + search_domain_filter: args.search_domain_filter as string[] | undefined, + }; + switch (name) { case "perplexity_ask": { if (!Array.isArray(args.messages)) { throw new Error("Invalid arguments for perplexity_ask: 'messages' must be an array"); } - // Invoke the chat completion function with the provided messages + // Invoke the chat completion function with the provided messages and filters const messages = args.messages; - const result = await performChatCompletion(messages, "sonar-pro"); + const result = await performChatCompletion(messages, "sonar-pro", filters); return { content: [{ type: "text", text: result }], isError: false, @@ -247,9 +497,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!Array.isArray(args.messages)) { throw new Error("Invalid arguments for perplexity_research: 'messages' must be an array"); } - // Invoke the chat completion function with the provided messages using the deep research model + // Invoke the chat completion function with the provided messages using the deep research model and filters const messages = args.messages; - const result = await performChatCompletion(messages, "sonar-deep-research"); + const result = await performChatCompletion(messages, "sonar-deep-research", filters); return { content: [{ type: "text", text: result }], isError: false, @@ -259,9 +509,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!Array.isArray(args.messages)) { throw new Error("Invalid arguments for perplexity_reason: 'messages' must be an array"); } - // Invoke the chat completion function with the provided messages using the reasoning model + // Invoke the chat completion function with the provided messages using the reasoning model and filters const messages = args.messages; - const result = await performChatCompletion(messages, "sonar-reasoning-pro"); + const result = await performChatCompletion(messages, "sonar-reasoning-pro", filters); return { content: [{ type: "text", text: result }], isError: false,