Skip to content

Add domain filtering #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ Desktop.ini
*.tsv
*.jsonl
:

# Ignore build output
dist/
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,27 @@ Please refer to the official [DeepWiki page](https://deepwiki.com/ppl-ai/modelco
## Tools

- **perplexity_ask**
- Engage in a conversation with the Sonar API for live web searches.
- Engages in a conversation using the Sonar API.
- **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.
- `model` (string, optional): The model to use for the completion. Can be `sonar` or `sonar-pro`. Defaults to `sonar-pro`.
- `search_domain_filter` (array of strings, optional): A list of domains to limit search results to (max 10). To denylist a domain, prefix it with a `-`.

- **perplexity_research**
- Performs deep research using the Perplexity API.
- **Inputs:**
- `messages` (array): An array of conversation messages.
- `search_domain_filter` (array of strings, optional): A list of domains to limit search results to (max 10). To denylist a domain, prefix it with a `-`.

- **perplexity_reason**
- Performs reasoning tasks using the Perplexity API.
- **Inputs:**
- `messages` (array): An array of conversation messages.
- `model` (string, optional): The model to use for reasoning. Can be `sonar-reasoning` or `sonar-reasoning-pro`. Defaults to `sonar-reasoning-pro`.
- `search_domain_filter` (array of strings, optional): A list of domains to limit search results to (max 10). To denylist a domain, prefix it with a `-`.

## Configuration

Expand All @@ -49,7 +64,21 @@ cd modelcontextprotocol/perplexity-ask && npm install
2. Follow the account setup instructions and generate your API key from the developer dashboard.
3. Set the API key in your environment as `PERPLEXITY_API_KEY`.

### Step 3: Configure Claude Desktop
### Step 3: Domain Filtering (Optional)

You can control the domains that Perplexity searches by using the `search_domain_filter` parameter. The configuration follows this hierarchy:

1. **User Input**: Provided directly in the tool call.
2. **Environment Variable**: If no user input is provided, the server will use the `PERPLEXITY_SEARCH_DOMAIN_FILTER` environment variable.
3. **Default**: If neither of the above is set, no domain filter will be applied.

To set the environment variable, you can add it to your shell configuration (e.g., `.zshrc`, `.bashrc`) or a `.env` file:

```bash
export PERPLEXITY_SEARCH_DOMAIN_FILTER="github.com,docs.pm,wikipedia.org"
```

### Step 4: Configure Claude Desktop

1. Download Claude desktop [here](https://claude.ai/download).

Expand Down
124 changes: 109 additions & 15 deletions perplexity-ask/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ const PERPLEXITY_ASK_TOOL: Tool = {
},
description: "Array of conversation messages",
},
model: {
type: "string",
description:
"The model to use for the completion. Can be 'sonar' or 'sonar-pro'. Defaults to 'sonar-pro'.",
enum: ["sonar", "sonar-pro"],
},
search_domain_filter: {
type: "array",
items: {
type: "string",
},
description:
"A list of domains to limit search results to. Max 10. Add a - at the beginning of the domain string for denylisting.",
},
},
required: ["messages"],
},
Expand Down Expand Up @@ -76,6 +90,14 @@ const PERPLEXITY_RESEARCH_TOOL: Tool = {
},
description: "Array of conversation messages",
},
search_domain_filter: {
type: "array",
items: {
type: "string",
},
description:
"A list of domains to limit search results to. Max 10. Add a - at the beginning of the domain string for denylisting.",
},
},
required: ["messages"],
},
Expand Down Expand Up @@ -112,6 +134,20 @@ const PERPLEXITY_REASON_TOOL: Tool = {
},
description: "Array of conversation messages",
},
model: {
type: "string",
description:
"The model to use for reasoning. Can be 'sonar-reasoning' or 'sonar-reasoning-pro'. Defaults to 'sonar-reasoning-pro'.",
enum: ["sonar-reasoning", "sonar-reasoning-pro"],
},
search_domain_filter: {
type: "array",
items: {
type: "string",
},
description:
"A list of domains to limit search results to. Max 10. Add a - at the beginning of the domain string for denylisting.",
},
},
required: ["messages"],
},
Expand All @@ -123,6 +159,7 @@ if (!PERPLEXITY_API_KEY) {
console.error("Error: PERPLEXITY_API_KEY environment variable is required");
process.exit(1);
}
const DOMAIN_FILTER_ENV = process.env.PERPLEXITY_SEARCH_DOMAIN_FILTER;

/**
* Performs a chat completion by sending a request to the Perplexity API.
Expand All @@ -135,18 +172,23 @@ if (!PERPLEXITY_API_KEY) {
*/
async function performChatCompletion(
messages: Array<{ role: string; content: string }>,
model: string = "sonar-pro"
model: string = "sonar-pro",
search_domain_filter?: string[]
): Promise<string> {
// 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
};

if (search_domain_filter && search_domain_filter.length > 0) {
if (search_domain_filter.length > 10) {
throw new Error("search_domain_filter cannot contain more than 10 domains.");
}
body.search_domain_filter = search_domain_filter;
}

let response;
try {
response = await fetch(url.toString(), {
Expand Down Expand Up @@ -182,7 +224,7 @@ async function performChatCompletion(
throw new Error(`Failed to parse JSON response from Perplexity API: ${jsonError}`);
}

// Directly retrieve the main message content from the response
// Directly retrieve the main message content from the response
let messageContent = data.choices[0].message.content;

// If citations are provided, append them to the message content
Expand Down Expand Up @@ -230,38 +272,88 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (!args) {
throw new Error("No arguments provided");
}

let search_domain_filter = args.search_domain_filter as (string[] | undefined);

// Hierarchy: user input > environment variable > default
if (search_domain_filter === undefined) {
if (DOMAIN_FILTER_ENV) {
search_domain_filter = DOMAIN_FILTER_ENV.split(',').map(d => d.trim());
} else {
search_domain_filter = [];
}
}

if (search_domain_filter && (!Array.isArray(search_domain_filter) || !search_domain_filter.every(item => typeof item === 'string'))) {
throw new Error(`Invalid arguments for ${name}: search_domain_filter must be an array of strings`);
}

switch (name) {
case "perplexity_ask": {
if (!Array.isArray(args.messages)) {
throw new Error("Invalid arguments for perplexity_ask: 'messages' must be an array");
throw new Error(
"Invalid arguments for perplexity_ask: 'messages' must be an array"
);
}
// Invoke the chat completion function with the provided messages
const messages = args.messages;
const result = await performChatCompletion(messages, "sonar-pro");
const model = args.model ?? "sonar-pro";
if (
typeof model !== "string" ||
!["sonar", "sonar-pro"].includes(model)
) {
throw new Error(
"Invalid model for perplexity_ask. Must be 'sonar' or 'sonar-pro'."
);
}
const result = await performChatCompletion(
messages,
model,
search_domain_filter
);
return {
content: [{ type: "text", text: result }],
isError: false,
};
}
case "perplexity_research": {
if (!Array.isArray(args.messages)) {
throw new Error("Invalid arguments for perplexity_research: 'messages' must be an array");
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
const messages = args.messages;
const result = await performChatCompletion(messages, "sonar-deep-research");
const result = await performChatCompletion(
messages,
"sonar-deep-research",
search_domain_filter
);
return {
content: [{ type: "text", text: result }],
isError: false,
};
}
case "perplexity_reason": {
if (!Array.isArray(args.messages)) {
throw new Error("Invalid arguments for perplexity_reason: 'messages' must be an array");
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
const messages = args.messages;
const result = await performChatCompletion(messages, "sonar-reasoning-pro");
const model = args.model ?? "sonar-reasoning-pro";
if (
typeof model !== "string" ||
!["sonar-reasoning", "sonar-reasoning-pro"].includes(model)
) {
throw new Error(
"Invalid model for perplexity_reason. Must be 'sonar-reasoning' or 'sonar-reasoning-pro'."
);
}
const result = await performChatCompletion(
messages,
model,
search_domain_filter
);
return {
content: [{ type: "text", text: result }],
isError: false,
Expand All @@ -280,7 +372,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
text: `Error: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
Expand Down
Loading