@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 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
+ });