@kkaminsk/modelcontextprotocol 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/buildCommonOptions.test.js +183 -0
- package/dist/formatMultiQueryResults.test.js +88 -0
- package/dist/formatSearchResults.test.js +55 -0
- package/dist/index.js +1224 -0
- package/dist/utils.js +124 -0
- package/dist/vitest.config.js +15 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 perplexity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Perplexity MCP Server
|
|
2
|
+
|
|
3
|
+
An unofficial Model Context Protocol (MCP) server fork that provides AI assistants with Perplexity API capabilities including real-time web search, deep research, and advanced reasoning.
|
|
4
|
+
|
|
5
|
+
## Available Tools
|
|
6
|
+
|
|
7
|
+
### perplexity_ask
|
|
8
|
+
Real-time AI-powered answers with web search. Supports model selection for balancing speed and quality.
|
|
9
|
+
|
|
10
|
+
| Parameter | Type | Description |
|
|
11
|
+
|-----------|------|-------------|
|
|
12
|
+
| `messages` | array | **Required.** Conversation messages with `role` and `content` |
|
|
13
|
+
| `model` | string | `sonar` (fast/cheap) or `sonar-pro` (high quality, default) |
|
|
14
|
+
| `stream` | boolean | Enable streaming responses (default: false) |
|
|
15
|
+
| `return_images` | boolean | Include relevant images in response (default: false) |
|
|
16
|
+
| `return_related_questions` | boolean | Include follow-up suggestions (default: false) |
|
|
17
|
+
|
|
18
|
+
### perplexity_research
|
|
19
|
+
Deep, comprehensive research using the `sonar-deep-research` model. Ideal for thorough analysis and detailed reports.
|
|
20
|
+
|
|
21
|
+
| Parameter | Type | Description |
|
|
22
|
+
|-----------|------|-------------|
|
|
23
|
+
| `messages` | array | **Required.** Conversation messages with `role` and `content` |
|
|
24
|
+
| `reasoning_effort` | string | `low`, `medium` (default), or `high` for research depth |
|
|
25
|
+
| `return_images` | boolean | Include relevant images in response (default: false) |
|
|
26
|
+
| `return_related_questions` | boolean | Include follow-up suggestions (default: false) |
|
|
27
|
+
|
|
28
|
+
### perplexity_reason
|
|
29
|
+
Advanced reasoning and problem-solving using the `sonar-reasoning-pro` model. Perfect for complex analytical tasks.
|
|
30
|
+
|
|
31
|
+
| Parameter | Type | Description |
|
|
32
|
+
|-----------|------|-------------|
|
|
33
|
+
| `messages` | array | **Required.** Conversation messages with `role` and `content` |
|
|
34
|
+
| `stream` | boolean | Enable streaming responses (default: false) |
|
|
35
|
+
|
|
36
|
+
### perplexity_search
|
|
37
|
+
Direct web search using the Perplexity Search API. Supports single or batch queries.
|
|
38
|
+
|
|
39
|
+
| Parameter | Type | Description |
|
|
40
|
+
|-----------|------|-------------|
|
|
41
|
+
| `query` | string or array | **Required.** Single query or up to 5 queries for batch search |
|
|
42
|
+
| `max_results` | number | Results per query (1-20, default: 10) |
|
|
43
|
+
| `max_tokens_per_page` | number | Tokens per webpage (256-2048, default: 1024) |
|
|
44
|
+
| `country` | string | ISO 3166-1 alpha-2 code for regional results (e.g., `US`, `GB`) |
|
|
45
|
+
|
|
46
|
+
### perplexity_research_async
|
|
47
|
+
Start an async deep research job for complex queries that may take several minutes.
|
|
48
|
+
|
|
49
|
+
| Parameter | Type | Description |
|
|
50
|
+
|-----------|------|-------------|
|
|
51
|
+
| `messages` | array | **Required.** Conversation messages with `role` and `content` |
|
|
52
|
+
| `reasoning_effort` | string | `low`, `medium` (default), or `high` for research depth |
|
|
53
|
+
| `search_domain_filter` | array | Domain allowlist/denylist (max 20) |
|
|
54
|
+
|
|
55
|
+
Returns a `request_id` to poll with `perplexity_research_status`.
|
|
56
|
+
|
|
57
|
+
### perplexity_research_status
|
|
58
|
+
Check status and retrieve results from an async research job.
|
|
59
|
+
|
|
60
|
+
| Parameter | Type | Description |
|
|
61
|
+
|-----------|------|-------------|
|
|
62
|
+
| `request_id` | string | **Required.** The request_id from `perplexity_research_async` |
|
|
63
|
+
|
|
64
|
+
## Common Parameters
|
|
65
|
+
|
|
66
|
+
The following parameters are available on `perplexity_ask`, `perplexity_research`, and `perplexity_reason`:
|
|
67
|
+
|
|
68
|
+
### Generation Controls
|
|
69
|
+
|
|
70
|
+
| Parameter | Type | Description |
|
|
71
|
+
|-----------|------|-------------|
|
|
72
|
+
| `temperature` | number | Randomness (0-2). Default: 0.2 |
|
|
73
|
+
| `max_tokens` | integer | Maximum response tokens |
|
|
74
|
+
| `top_p` | number | Nucleus sampling (0-1). Default: 0.9 |
|
|
75
|
+
| `top_k` | integer | Top-k sampling. 0 = disabled |
|
|
76
|
+
|
|
77
|
+
### Search Filters
|
|
78
|
+
|
|
79
|
+
| Parameter | Type | Description |
|
|
80
|
+
|-----------|------|-------------|
|
|
81
|
+
| `search_domain_filter` | array | Domain list (max 20). Prefix with `-` to exclude |
|
|
82
|
+
| `search_mode` | string | `web` (default), `academic`, or `sec` (SEC filings) |
|
|
83
|
+
|
|
84
|
+
### Date Filters
|
|
85
|
+
|
|
86
|
+
| Parameter | Type | Description |
|
|
87
|
+
|-----------|------|-------------|
|
|
88
|
+
| `search_recency_filter` | string | `day`, `week`, `month`, or `year` |
|
|
89
|
+
| `search_after_date` | string | Only results after date (MM/DD/YYYY) |
|
|
90
|
+
| `search_before_date` | string | Only results before date (MM/DD/YYYY) |
|
|
91
|
+
| `last_updated_after` | string | Only results updated after date (MM/DD/YYYY) |
|
|
92
|
+
| `last_updated_before` | string | Only results updated before date (MM/DD/YYYY) |
|
|
93
|
+
|
|
94
|
+
> **Note:** `perplexity_search` supports `search_domain_filter` and all date filters, but not generation controls or `search_mode`.
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
### Windows Installer (Recommended for Windows)
|
|
99
|
+
|
|
100
|
+
Download and run the MSI installer for a one-click setup:
|
|
101
|
+
|
|
102
|
+
1. Download `PerplexityMCP.msi` from the [Releases](https://github.com/perplexityai/modelcontextprotocol/releases) page
|
|
103
|
+
2. Run the installer and enter your Perplexity API key when prompted
|
|
104
|
+
3. The installer automatically configures Claude Desktop, Claude Code, Cursor, and Codex
|
|
105
|
+
|
|
106
|
+
The installer bundles Node.js, so no prerequisites are required.
|
|
107
|
+
|
|
108
|
+
**Silent install:**
|
|
109
|
+
```powershell
|
|
110
|
+
msiexec /i PerplexityMCP.msi PERPLEXITY_API_KEY="your_key_here" /qn
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### npm / npx
|
|
114
|
+
|
|
115
|
+
For manual installation or non-Windows platforms:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx @perplexity-ai/mcp-server
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Or install globally:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm install -g @perplexity-ai/mcp-server
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Configuration
|
|
128
|
+
|
|
129
|
+
### Get Your API Key
|
|
130
|
+
1. Get your Perplexity API Key from the [API Portal](https://www.perplexity.ai/account/api)
|
|
131
|
+
2. Set it as an environment variable: `PERPLEXITY_API_KEY=your_key_here`
|
|
132
|
+
3. (Optional) Set a timeout for requests: `PERPLEXITY_TIMEOUT_MS=600000` (default: 5 minutes)
|
|
133
|
+
|
|
134
|
+
### Claude Code
|
|
135
|
+
|
|
136
|
+
Run in your terminal:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
claude mcp add perplexity --transport stdio --env PERPLEXITY_API_KEY=your_key_here -- npx -y perplexity-mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or add to your `claude.json`:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
"mcpServers": {
|
|
146
|
+
"perplexity": {
|
|
147
|
+
"type": "stdio",
|
|
148
|
+
"command": "npx",
|
|
149
|
+
"args": ["-y", "perplexity-mcp"],
|
|
150
|
+
"env": {
|
|
151
|
+
"PERPLEXITY_API_KEY": "your_key_here",
|
|
152
|
+
"PERPLEXITY_TIMEOUT_MS": "600000"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Cursor
|
|
159
|
+
|
|
160
|
+
Add to your `mcp.json`:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"mcpServers": {
|
|
165
|
+
"perplexity": {
|
|
166
|
+
"command": "npx",
|
|
167
|
+
"args": ["-y", "@perplexity-ai/mcp-server"],
|
|
168
|
+
"env": {
|
|
169
|
+
"PERPLEXITY_API_KEY": "your_key_here",
|
|
170
|
+
"PERPLEXITY_TIMEOUT_MS": "600000"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Codex
|
|
178
|
+
|
|
179
|
+
Run in your terminal:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
codex mcp add perplexity --env PERPLEXITY_API_KEY=your_key_here -- npx -y @perplexity-ai/mcp-server
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Claude Desktop
|
|
186
|
+
|
|
187
|
+
Add to your `claude_desktop_config.json`:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"mcpServers": {
|
|
192
|
+
"perplexity": {
|
|
193
|
+
"command": "npx",
|
|
194
|
+
"args": ["-y", "@perplexity-ai/mcp-server"],
|
|
195
|
+
"env": {
|
|
196
|
+
"PERPLEXITY_API_KEY": "your_key_here",
|
|
197
|
+
"PERPLEXITY_TIMEOUT_MS": "600000"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Other MCP Clients
|
|
205
|
+
|
|
206
|
+
For any MCP-compatible client, use:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npx @perplexity-ai/mcp-server
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Troubleshooting
|
|
213
|
+
|
|
214
|
+
- **API Key Issues**: Ensure `PERPLEXITY_API_KEY` is set correctly
|
|
215
|
+
- **Connection Errors**: Check your internet connection and API key validity
|
|
216
|
+
- **Tool Not Found**: Make sure the package is installed and the command path is correct
|
|
217
|
+
- **Timeout Errors**: For long research queries, increase `PERPLEXITY_TIMEOUT_MS`
|
|
218
|
+
|
|
219
|
+
For support, visit [community.perplexity.ai](https://community.perplexity.ai) or [file an issue](https://github.com/perplexityai/modelcontextprotocol/issues).
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildCommonOptions, MAX_DOMAIN_FILTERS, MAX_BATCH_QUERIES, DEFAULT_MODEL, DEFAULT_TIMEOUT_MS, } from "./utils.js";
|
|
3
|
+
describe("buildCommonOptions", () => {
|
|
4
|
+
describe("search_domain_filter validation", () => {
|
|
5
|
+
it("should extract valid string array for search_domain_filter", () => {
|
|
6
|
+
const args = { search_domain_filter: ["example.com", "test.org"] };
|
|
7
|
+
const result = buildCommonOptions(args);
|
|
8
|
+
expect(result.search_domain_filter).toEqual(["example.com", "test.org"]);
|
|
9
|
+
});
|
|
10
|
+
it("should filter out non-string values from search_domain_filter", () => {
|
|
11
|
+
const args = { search_domain_filter: ["valid.com", 123, null, "also-valid.org"] };
|
|
12
|
+
const result = buildCommonOptions(args);
|
|
13
|
+
expect(result.search_domain_filter).toEqual(["valid.com", "also-valid.org"]);
|
|
14
|
+
});
|
|
15
|
+
it("should not set search_domain_filter for empty array", () => {
|
|
16
|
+
const args = { search_domain_filter: [] };
|
|
17
|
+
const result = buildCommonOptions(args);
|
|
18
|
+
expect(result.search_domain_filter).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
it("should not set search_domain_filter for non-array", () => {
|
|
21
|
+
const args = { search_domain_filter: "not-an-array" };
|
|
22
|
+
const result = buildCommonOptions(args);
|
|
23
|
+
expect(result.search_domain_filter).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe("temperature validation", () => {
|
|
27
|
+
it("should extract valid temperature", () => {
|
|
28
|
+
const args = { temperature: 0.5 };
|
|
29
|
+
const result = buildCommonOptions(args);
|
|
30
|
+
expect(result.temperature).toBe(0.5);
|
|
31
|
+
});
|
|
32
|
+
it("should accept temperature at boundaries (0 and 2)", () => {
|
|
33
|
+
expect(buildCommonOptions({ temperature: 0 }).temperature).toBe(0);
|
|
34
|
+
expect(buildCommonOptions({ temperature: 2 }).temperature).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
it("should not set temperature for non-number", () => {
|
|
37
|
+
const args = { temperature: "0.5" };
|
|
38
|
+
const result = buildCommonOptions(args);
|
|
39
|
+
expect(result.temperature).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("max_tokens validation", () => {
|
|
43
|
+
it("should extract valid max_tokens", () => {
|
|
44
|
+
const args = { max_tokens: 1000 };
|
|
45
|
+
const result = buildCommonOptions(args);
|
|
46
|
+
expect(result.max_tokens).toBe(1000);
|
|
47
|
+
});
|
|
48
|
+
it("should not set max_tokens for non-number", () => {
|
|
49
|
+
const args = { max_tokens: "1000" };
|
|
50
|
+
const result = buildCommonOptions(args);
|
|
51
|
+
expect(result.max_tokens).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("top_p validation", () => {
|
|
55
|
+
it("should extract valid top_p", () => {
|
|
56
|
+
const args = { top_p: 0.9 };
|
|
57
|
+
const result = buildCommonOptions(args);
|
|
58
|
+
expect(result.top_p).toBe(0.9);
|
|
59
|
+
});
|
|
60
|
+
it("should accept top_p at boundaries (0 and 1)", () => {
|
|
61
|
+
expect(buildCommonOptions({ top_p: 0 }).top_p).toBe(0);
|
|
62
|
+
expect(buildCommonOptions({ top_p: 1 }).top_p).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
it("should not set top_p for non-number", () => {
|
|
65
|
+
const args = { top_p: "0.9" };
|
|
66
|
+
const result = buildCommonOptions(args);
|
|
67
|
+
expect(result.top_p).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("top_k validation", () => {
|
|
71
|
+
it("should extract valid top_k", () => {
|
|
72
|
+
const args = { top_k: 50 };
|
|
73
|
+
const result = buildCommonOptions(args);
|
|
74
|
+
expect(result.top_k).toBe(50);
|
|
75
|
+
});
|
|
76
|
+
it("should accept top_k of 0 (disabled)", () => {
|
|
77
|
+
const args = { top_k: 0 };
|
|
78
|
+
const result = buildCommonOptions(args);
|
|
79
|
+
expect(result.top_k).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
it("should not set top_k for non-number", () => {
|
|
82
|
+
const args = { top_k: "50" };
|
|
83
|
+
const result = buildCommonOptions(args);
|
|
84
|
+
expect(result.top_k).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("search_mode validation", () => {
|
|
88
|
+
it("should extract valid search_mode values", () => {
|
|
89
|
+
expect(buildCommonOptions({ search_mode: "web" }).search_mode).toBe("web");
|
|
90
|
+
expect(buildCommonOptions({ search_mode: "academic" }).search_mode).toBe("academic");
|
|
91
|
+
expect(buildCommonOptions({ search_mode: "sec" }).search_mode).toBe("sec");
|
|
92
|
+
});
|
|
93
|
+
it("should not set invalid search_mode", () => {
|
|
94
|
+
const args = { search_mode: "invalid" };
|
|
95
|
+
const result = buildCommonOptions(args);
|
|
96
|
+
expect(result.search_mode).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
it("should not set search_mode for non-string", () => {
|
|
99
|
+
const args = { search_mode: 123 };
|
|
100
|
+
const result = buildCommonOptions(args);
|
|
101
|
+
expect(result.search_mode).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe("search_recency_filter validation", () => {
|
|
105
|
+
it("should extract valid search_recency_filter values", () => {
|
|
106
|
+
expect(buildCommonOptions({ search_recency_filter: "day" }).search_recency_filter).toBe("day");
|
|
107
|
+
expect(buildCommonOptions({ search_recency_filter: "week" }).search_recency_filter).toBe("week");
|
|
108
|
+
expect(buildCommonOptions({ search_recency_filter: "month" }).search_recency_filter).toBe("month");
|
|
109
|
+
expect(buildCommonOptions({ search_recency_filter: "year" }).search_recency_filter).toBe("year");
|
|
110
|
+
});
|
|
111
|
+
it("should not set invalid search_recency_filter", () => {
|
|
112
|
+
const args = { search_recency_filter: "hour" };
|
|
113
|
+
const result = buildCommonOptions(args);
|
|
114
|
+
expect(result.search_recency_filter).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("date filters", () => {
|
|
118
|
+
it("should extract date filter strings", () => {
|
|
119
|
+
const args = {
|
|
120
|
+
search_after_date: "01/01/2024",
|
|
121
|
+
search_before_date: "12/31/2024",
|
|
122
|
+
last_updated_after: "06/01/2024",
|
|
123
|
+
last_updated_before: "09/30/2024",
|
|
124
|
+
};
|
|
125
|
+
const result = buildCommonOptions(args);
|
|
126
|
+
expect(result.search_after_date).toBe("01/01/2024");
|
|
127
|
+
expect(result.search_before_date).toBe("12/31/2024");
|
|
128
|
+
expect(result.last_updated_after).toBe("06/01/2024");
|
|
129
|
+
expect(result.last_updated_before).toBe("09/30/2024");
|
|
130
|
+
});
|
|
131
|
+
it("should not set date filters for non-strings", () => {
|
|
132
|
+
const args = {
|
|
133
|
+
search_after_date: 123,
|
|
134
|
+
search_before_date: null,
|
|
135
|
+
};
|
|
136
|
+
const result = buildCommonOptions(args);
|
|
137
|
+
expect(result.search_after_date).toBeUndefined();
|
|
138
|
+
expect(result.search_before_date).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe("boolean options", () => {
|
|
142
|
+
it("should extract return_images when true", () => {
|
|
143
|
+
const args = { return_images: true };
|
|
144
|
+
const result = buildCommonOptions(args);
|
|
145
|
+
expect(result.return_images).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
it("should not set return_images when false", () => {
|
|
148
|
+
const args = { return_images: false };
|
|
149
|
+
const result = buildCommonOptions(args);
|
|
150
|
+
expect(result.return_images).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
it("should extract return_related_questions when true", () => {
|
|
153
|
+
const args = { return_related_questions: true };
|
|
154
|
+
const result = buildCommonOptions(args);
|
|
155
|
+
expect(result.return_related_questions).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it("should not set return_related_questions when false", () => {
|
|
158
|
+
const args = { return_related_questions: false };
|
|
159
|
+
const result = buildCommonOptions(args);
|
|
160
|
+
expect(result.return_related_questions).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe("empty input", () => {
|
|
164
|
+
it("should return empty object for empty args", () => {
|
|
165
|
+
const result = buildCommonOptions({});
|
|
166
|
+
expect(result).toEqual({});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("Constants", () => {
|
|
171
|
+
it("should have correct MAX_DOMAIN_FILTERS value", () => {
|
|
172
|
+
expect(MAX_DOMAIN_FILTERS).toBe(20);
|
|
173
|
+
});
|
|
174
|
+
it("should have correct MAX_BATCH_QUERIES value", () => {
|
|
175
|
+
expect(MAX_BATCH_QUERIES).toBe(5);
|
|
176
|
+
});
|
|
177
|
+
it("should have correct DEFAULT_MODEL value", () => {
|
|
178
|
+
expect(DEFAULT_MODEL).toBe("sonar-pro");
|
|
179
|
+
});
|
|
180
|
+
it("should have correct DEFAULT_TIMEOUT_MS value", () => {
|
|
181
|
+
expect(DEFAULT_TIMEOUT_MS).toBe(300000);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatMultiQueryResults, } from "./utils.js";
|
|
3
|
+
describe("formatMultiQueryResults", () => {
|
|
4
|
+
it("should format a single query result", () => {
|
|
5
|
+
const results = [
|
|
6
|
+
{
|
|
7
|
+
query: "test query",
|
|
8
|
+
data: {
|
|
9
|
+
results: [{ title: "Result 1", url: "https://example.com" }],
|
|
10
|
+
},
|
|
11
|
+
error: null,
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
const result = formatMultiQueryResults(results);
|
|
15
|
+
expect(result).toContain('## Query 1: "test query"');
|
|
16
|
+
expect(result).toContain("Found 1 results");
|
|
17
|
+
expect(result).toContain("**Result 1**");
|
|
18
|
+
});
|
|
19
|
+
it("should format multiple query results", () => {
|
|
20
|
+
const results = [
|
|
21
|
+
{
|
|
22
|
+
query: "first query",
|
|
23
|
+
data: {
|
|
24
|
+
results: [{ title: "First Result", url: "https://first.com" }],
|
|
25
|
+
},
|
|
26
|
+
error: null,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
query: "second query",
|
|
30
|
+
data: {
|
|
31
|
+
results: [{ title: "Second Result", url: "https://second.com" }],
|
|
32
|
+
},
|
|
33
|
+
error: null,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
const result = formatMultiQueryResults(results);
|
|
37
|
+
expect(result).toContain('## Query 1: "first query"');
|
|
38
|
+
expect(result).toContain('## Query 2: "second query"');
|
|
39
|
+
expect(result).toContain("**First Result**");
|
|
40
|
+
expect(result).toContain("**Second Result**");
|
|
41
|
+
expect(result).toContain("---"); // Separator between queries
|
|
42
|
+
});
|
|
43
|
+
it("should format a query with error", () => {
|
|
44
|
+
const results = [
|
|
45
|
+
{
|
|
46
|
+
query: "error query",
|
|
47
|
+
data: null,
|
|
48
|
+
error: "API rate limit exceeded",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
const result = formatMultiQueryResults(results);
|
|
52
|
+
expect(result).toContain('## Query 1: "error query"');
|
|
53
|
+
expect(result).toContain("**Error:** API rate limit exceeded");
|
|
54
|
+
});
|
|
55
|
+
it("should format mixed success and error results", () => {
|
|
56
|
+
const results = [
|
|
57
|
+
{
|
|
58
|
+
query: "successful query",
|
|
59
|
+
data: {
|
|
60
|
+
results: [{ title: "Good Result", url: "https://good.com" }],
|
|
61
|
+
},
|
|
62
|
+
error: null,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
query: "failed query",
|
|
66
|
+
data: null,
|
|
67
|
+
error: "Network timeout",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
const result = formatMultiQueryResults(results);
|
|
71
|
+
expect(result).toContain('## Query 1: "successful query"');
|
|
72
|
+
expect(result).toContain("**Good Result**");
|
|
73
|
+
expect(result).toContain('## Query 2: "failed query"');
|
|
74
|
+
expect(result).toContain("**Error:** Network timeout");
|
|
75
|
+
});
|
|
76
|
+
it("should handle query with null data and no error", () => {
|
|
77
|
+
const results = [
|
|
78
|
+
{
|
|
79
|
+
query: "empty query",
|
|
80
|
+
data: null,
|
|
81
|
+
error: null,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
const result = formatMultiQueryResults(results);
|
|
85
|
+
expect(result).toContain('## Query 1: "empty query"');
|
|
86
|
+
expect(result).toContain("No search results found.");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatSearchResults, } from "./utils.js";
|
|
3
|
+
describe("formatSearchResults", () => {
|
|
4
|
+
it("should return 'No search results found.' for empty results array", () => {
|
|
5
|
+
const data = { results: [] };
|
|
6
|
+
const result = formatSearchResults(data);
|
|
7
|
+
expect(result).toContain("Found 0 search results");
|
|
8
|
+
});
|
|
9
|
+
it("should return 'No search results found.' for undefined results", () => {
|
|
10
|
+
const data = {};
|
|
11
|
+
const result = formatSearchResults(data);
|
|
12
|
+
expect(result).toBe("No search results found.");
|
|
13
|
+
});
|
|
14
|
+
it("should format a single result with all fields", () => {
|
|
15
|
+
const data = {
|
|
16
|
+
results: [
|
|
17
|
+
{
|
|
18
|
+
title: "Test Title",
|
|
19
|
+
url: "https://example.com",
|
|
20
|
+
snippet: "This is a test snippet",
|
|
21
|
+
date: "2024-01-15",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
const result = formatSearchResults(data);
|
|
26
|
+
expect(result).toContain("Found 1 search results");
|
|
27
|
+
expect(result).toContain("**Test Title**");
|
|
28
|
+
expect(result).toContain("URL: https://example.com");
|
|
29
|
+
expect(result).toContain("This is a test snippet");
|
|
30
|
+
expect(result).toContain("Date: 2024-01-15");
|
|
31
|
+
});
|
|
32
|
+
it("should format multiple results", () => {
|
|
33
|
+
const data = {
|
|
34
|
+
results: [
|
|
35
|
+
{ title: "First Result", url: "https://first.com" },
|
|
36
|
+
{ title: "Second Result", url: "https://second.com" },
|
|
37
|
+
{ title: "Third Result", url: "https://third.com" },
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
const result = formatSearchResults(data);
|
|
41
|
+
expect(result).toContain("Found 3 search results");
|
|
42
|
+
expect(result).toContain("1. **First Result**");
|
|
43
|
+
expect(result).toContain("2. **Second Result**");
|
|
44
|
+
expect(result).toContain("3. **Third Result**");
|
|
45
|
+
});
|
|
46
|
+
it("should handle missing optional fields (snippet, date)", () => {
|
|
47
|
+
const data = {
|
|
48
|
+
results: [{ title: "Title Only", url: "https://example.com" }],
|
|
49
|
+
};
|
|
50
|
+
const result = formatSearchResults(data);
|
|
51
|
+
expect(result).toContain("**Title Only**");
|
|
52
|
+
expect(result).toContain("URL: https://example.com");
|
|
53
|
+
expect(result).not.toContain("Date:");
|
|
54
|
+
});
|
|
55
|
+
});
|