pplx-npx-search 0.3.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to pplx-cli will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.2] - 2026-05-26
9
+
10
+ ### Added
11
+ - **MCP server.** Added `pplx-mcp`, a stdio MCP server for Claude Desktop, Claude Code, Codex, and other MCP clients.
12
+ - **MCP tool surface.** Exposes search, labs, auth status, model listing, and Perplexity Computer artifact tools through MCP.
13
+
8
14
  ## [0.3.1] - 2026-05-22
9
15
 
10
16
  ### Changed
@@ -67,6 +73,7 @@ First public release worth telling people about. (v0.2.0 was unpublished before
67
73
  - SSE streaming for real-time answers
68
74
  - Optional Playwright and Chrome CDP transports
69
75
 
76
+ [0.3.2]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.1...v0.3.2
70
77
  [0.3.1]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.0...v0.3.1
71
78
  [0.3.0]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.2...v0.3.0
72
79
  [0.2.2]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.1...v0.2.2
package/README.md CHANGED
@@ -152,6 +152,42 @@ pplx search "research this topic" --json --raw --mode pro
152
152
  }
153
153
  ```
154
154
 
155
+ ### MCP Server
156
+
157
+ `pplx-mcp` exposes the same Perplexity workflow through a local stdio MCP server for Claude Desktop, Claude Code, Codex, and other MCP clients.
158
+
159
+ ```bash
160
+ pplx-mcp
161
+ ```
162
+
163
+ Typical global client registrations:
164
+
165
+ ```bash
166
+ claude mcp add --scope user pplx -- /path/to/node /path/to/pplx-mcp
167
+ codex mcp add pplx -- /path/to/node /path/to/pplx-mcp
168
+ ```
169
+
170
+ For Claude Desktop, add the server under `mcpServers` in `~/Library/Application Support/Claude/claude_desktop_config.json`:
171
+
172
+ ```json
173
+ {
174
+ "mcpServers": {
175
+ "pplx": {
176
+ "command": "/path/to/node",
177
+ "args": ["/path/to/pplx-mcp"]
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ The MCP server exposes:
184
+
185
+ - `pplx_search`: authenticated `search`, `reasoning`, and `deep-research` queries with sources and artifacts.
186
+ - `pplx_labs`: Labs model queries that do not require cookie auth.
187
+ - `pplx_auth_status`: validates the stored Perplexity browser cookies.
188
+ - `pplx_models`: lists known model aliases.
189
+ - `pplx_computer_create`, `pplx_computer_status`, `pplx_computer_read_task`, and `pplx_computer_import`: Perplexity Computer artifact handoff tools.
190
+
155
191
  ---
156
192
 
157
193
  ## Artifacts
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runMcpServer } from '../src/mcp-server.js';
3
+
4
+ runMcpServer().catch((error) => {
5
+ console.error('pplx MCP server error:', error?.message || error);
6
+ process.exit(1);
7
+ });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "pplx-npx-search",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for Perplexity AI with cookie-based auth. Headless, agent-friendly, no API key required.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "pplx": "bin/pplx.js"
7
+ "pplx": "bin/pplx.js",
8
+ "pplx-mcp": "bin/pplx-mcp.js"
8
9
  },
9
10
  "scripts": {
10
11
  "start": "node bin/pplx.js",
@@ -37,12 +38,14 @@
37
38
  "node": ">=20.0.0"
38
39
  },
39
40
  "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.29.0",
40
42
  "better-sqlite3": "^12.10.0",
41
43
  "chalk": "^5.3.0",
42
44
  "commander": "^12.0.0",
43
45
  "eventsource-parser": "^3.0.0",
44
46
  "ora": "^8.0.0",
45
47
  "playwright": "^1.58.1",
46
- "ws": "^8.16.0"
48
+ "ws": "^8.21.0",
49
+ "zod": "^4.4.3"
47
50
  }
48
51
  }
@@ -0,0 +1,352 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join, resolve, isAbsolute } from 'node:path';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import * as z from 'zod/v4';
6
+ import { loadConfig } from './config.js';
7
+ import { loadCookies } from './cookies.js';
8
+ import { testAuth } from './session.js';
9
+ import { search } from './search.js';
10
+ import { LabsClient } from './labs.js';
11
+ import { resolveTimeoutMs } from './timeout.js';
12
+ import { makeArtifactContext, resolveArtifactDir, writeStandardArtifact } from './artifacts.js';
13
+ import {
14
+ createComputerRun,
15
+ importComputerResult,
16
+ inspectComputerRun,
17
+ readTaskFile,
18
+ } from './computer.js';
19
+ import { MODEL_MAP, LABS_MODELS } from './constants.js';
20
+
21
+ const SERVER_NAME = 'pplx';
22
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
23
+ const SERVER_VERSION = pkg.version;
24
+ const SEARCH_MODES = ['auto', 'pro', 'reasoning', 'deep-research'];
25
+ const TRANSPORTS = ['auto', 'http', 'playwright', 'curl', 'chrome'];
26
+
27
+ const optionalArtifactArgs = {
28
+ out: z.string().optional().describe('Artifact root for this run. Defaults to ~/.config/pplx/config.json artifactDir.'),
29
+ artifactId: z.string().optional().describe('Deterministic artifact id for this run.'),
30
+ saveArtifact: z.boolean().optional().default(true).describe('Save the standard pplx artifact files.'),
31
+ };
32
+
33
+ function toTextResult(value) {
34
+ return {
35
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
36
+ structuredContent: value,
37
+ };
38
+ }
39
+
40
+ function normalizeSources(sources) {
41
+ if (!sources) return ['web'];
42
+ if (Array.isArray(sources)) return sources;
43
+ return String(sources).split(',').map((source) => source.trim()).filter(Boolean);
44
+ }
45
+
46
+ function normalizeSourceResult(result) {
47
+ return {
48
+ title: result.name || result.title || '',
49
+ url: result.url || '',
50
+ };
51
+ }
52
+
53
+ function mcpArtifactOpts(args = {}) {
54
+ if (args.saveArtifact === false) return { artifact: false };
55
+ return {
56
+ out: args.out,
57
+ artifactId: args.artifactId,
58
+ };
59
+ }
60
+
61
+ function transportOpts(transport, config) {
62
+ if (transport === 'chrome') return { chrome: true };
63
+ if (transport === 'playwright') return { playwright: true };
64
+ if (transport === 'curl') return { curl: true };
65
+ if (transport === 'http') return { playwright: false, chrome: false, curl: false };
66
+ return {
67
+ chrome: config.chrome,
68
+ playwright: config.playwright,
69
+ curl: config.curl,
70
+ };
71
+ }
72
+
73
+ async function assertAuthReady({ cookies, opts }) {
74
+ if (opts.chrome || opts.allowAnonymous) return;
75
+ if (Object.keys(cookies).length === 0) {
76
+ throw new Error('No Perplexity cookies stored. Run: pplx auth --browser auto');
77
+ }
78
+ const ok = await testAuth(cookies);
79
+ if (!ok) {
80
+ throw new Error('Stored Perplexity cookies are invalid or expired. Run: pplx auth --browser auto');
81
+ }
82
+ }
83
+
84
+ export async function runSearchTool(args) {
85
+ const config = loadConfig();
86
+ const mode = args.mode || 'pro';
87
+ const transport = args.transport || 'auto';
88
+ const opts = {
89
+ ...config,
90
+ ...transportOpts(transport, config),
91
+ mode,
92
+ model: args.model,
93
+ sources: normalizeSources(args.sources),
94
+ language: args.language || args.lang || 'en-US',
95
+ incognito: args.incognito ?? false,
96
+ allowAnonymous: args.allowAnonymous ?? false,
97
+ timeoutMs: undefined,
98
+ };
99
+ opts.timeoutMs = resolveTimeoutMs({ ...opts, timeoutMs: args.timeoutMs, mode });
100
+
101
+ const cookies = loadCookies() || {};
102
+ await assertAuthReady({ cookies, opts });
103
+
104
+ const artifactCtx = makeArtifactContext({
105
+ command: mode === 'deep-research' ? 'research' : mode === 'reasoning' ? 'reason' : 'search',
106
+ query: args.query,
107
+ opts: mcpArtifactOpts(args),
108
+ config,
109
+ });
110
+
111
+ let lastAnswer = '';
112
+ let lastData = null;
113
+ for await (const data of search(args.query, cookies, opts)) {
114
+ lastData = data;
115
+ if ((data.answer || '').length >= lastAnswer.length) {
116
+ lastAnswer = data.answer || lastAnswer;
117
+ }
118
+ }
119
+
120
+ const answer = lastData?.answer || lastAnswer || '';
121
+ if (!answer) throw new Error('No answer received from Perplexity.');
122
+
123
+ const sources = (lastData?.web_results || []).map(normalizeSourceResult);
124
+ const artifactInfo = writeStandardArtifact(artifactCtx, {
125
+ answer,
126
+ sources,
127
+ mode,
128
+ model: args.model || 'default',
129
+ });
130
+
131
+ return {
132
+ query: args.query,
133
+ answer,
134
+ sources,
135
+ mode,
136
+ model: args.model || 'default',
137
+ artifactDir: artifactInfo?.artifactDir || null,
138
+ artifactId: artifactInfo?.artifactId || null,
139
+ };
140
+ }
141
+
142
+ export async function runLabsTool(args) {
143
+ const config = loadConfig();
144
+ const model = args.model || 'sonar';
145
+ const artifactCtx = makeArtifactContext({
146
+ command: 'labs',
147
+ query: args.query,
148
+ opts: mcpArtifactOpts(args),
149
+ config,
150
+ });
151
+ const client = new LabsClient();
152
+ let answer = '';
153
+ const events = [];
154
+ try {
155
+ await client.connect();
156
+ for await (const data of client.ask(args.query, model)) {
157
+ events.push(data);
158
+ answer = data.output || answer;
159
+ }
160
+ } finally {
161
+ client.close();
162
+ }
163
+
164
+ const artifactInfo = writeStandardArtifact(artifactCtx, {
165
+ answer,
166
+ sources: [],
167
+ mode: 'labs',
168
+ model,
169
+ });
170
+
171
+ return {
172
+ query: args.query,
173
+ answer,
174
+ events,
175
+ mode: 'labs',
176
+ model,
177
+ artifactDir: artifactInfo?.artifactDir || null,
178
+ artifactId: artifactInfo?.artifactId || null,
179
+ };
180
+ }
181
+
182
+ export async function getAuthStatus() {
183
+ const cookies = loadCookies() || {};
184
+ const cookieCount = Object.keys(cookies).length;
185
+ if (cookieCount === 0) {
186
+ return {
187
+ authenticated: false,
188
+ cookieCount,
189
+ message: 'No cookies stored. Run: pplx auth --browser auto',
190
+ };
191
+ }
192
+ const authenticated = await testAuth(cookies);
193
+ return {
194
+ authenticated,
195
+ cookieCount,
196
+ message: authenticated
197
+ ? 'Stored Perplexity cookies are valid.'
198
+ : 'Stored Perplexity cookies are invalid or expired. Run: pplx auth --browser auto',
199
+ };
200
+ }
201
+
202
+ function resolveRunDir(run, out, config) {
203
+ if (isAbsolute(run) || run.includes('/')) return resolve(run);
204
+ return join(resolveArtifactDir({ out, config }), run);
205
+ }
206
+
207
+ export function createPplxMcpServer() {
208
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
209
+
210
+ server.registerTool('pplx_search', {
211
+ title: 'Perplexity Search',
212
+ description: 'Run an authenticated Perplexity search/reasoning/deep-research query and return answer, sources, and artifact paths.',
213
+ inputSchema: {
214
+ query: z.string().min(1).describe('Question or research prompt.'),
215
+ mode: z.enum(SEARCH_MODES).optional().default('pro').describe('Perplexity mode.'),
216
+ model: z.string().optional().describe('Optional model alias or raw Perplexity model id.'),
217
+ sources: z.array(z.string()).optional().default(['web']).describe('Source types such as web, scholar, or social.'),
218
+ language: z.string().optional().default('en-US').describe('Language code.'),
219
+ incognito: z.boolean().optional().default(false).describe('Do not save the query to Perplexity history.'),
220
+ transport: z.enum(TRANSPORTS).optional().default('auto').describe('Transport override for Perplexity calls.'),
221
+ timeoutMs: z.union([z.string(), z.number()]).optional().describe('Overall stream timeout, e.g. 120s, 10m, or milliseconds.'),
222
+ allowAnonymous: z.boolean().optional().default(false).describe('Allow anonymous Perplexity responses when cookies are missing or expired.'),
223
+ ...optionalArtifactArgs,
224
+ },
225
+ annotations: {
226
+ title: 'Perplexity Search',
227
+ readOnlyHint: false,
228
+ openWorldHint: true,
229
+ },
230
+ }, async (args) => toTextResult(await runSearchTool(args)));
231
+
232
+ server.registerTool('pplx_labs', {
233
+ title: 'Perplexity Labs',
234
+ description: 'Query Perplexity Labs models without browser-cookie auth.',
235
+ inputSchema: {
236
+ query: z.string().min(1).describe('Question or prompt.'),
237
+ model: z.enum(LABS_MODELS).optional().default('sonar').describe('Labs model.'),
238
+ ...optionalArtifactArgs,
239
+ },
240
+ annotations: {
241
+ title: 'Perplexity Labs',
242
+ readOnlyHint: false,
243
+ openWorldHint: true,
244
+ },
245
+ }, async (args) => toTextResult(await runLabsTool(args)));
246
+
247
+ server.registerTool('pplx_auth_status', {
248
+ title: 'Perplexity Auth Status',
249
+ description: 'Check whether stored Perplexity cookies are present and authenticated.',
250
+ inputSchema: {},
251
+ annotations: {
252
+ title: 'Perplexity Auth Status',
253
+ readOnlyHint: true,
254
+ openWorldHint: false,
255
+ },
256
+ }, async () => toTextResult(await getAuthStatus()));
257
+
258
+ server.registerTool('pplx_models', {
259
+ title: 'Perplexity Models',
260
+ description: 'List known Perplexity model aliases exposed by pplx-cli.',
261
+ inputSchema: {},
262
+ annotations: {
263
+ title: 'Perplexity Models',
264
+ readOnlyHint: true,
265
+ openWorldHint: false,
266
+ },
267
+ }, async () => toTextResult({ modes: MODEL_MAP, labs: LABS_MODELS }));
268
+
269
+ server.registerTool('pplx_computer_create', {
270
+ title: 'Create Perplexity Computer Handoff',
271
+ description: 'Create a Perplexity Computer artifact handoff folder containing task.md, result.schema.json, and computer-result.json.',
272
+ inputSchema: {
273
+ task: z.string().min(1).describe('Live web task for Perplexity Computer.'),
274
+ template: z.literal('compare').optional().default('compare').describe('Computer task template.'),
275
+ out: z.string().optional().describe('Artifact root for this run.'),
276
+ artifactId: z.string().optional().describe('Deterministic artifact id for this run.'),
277
+ },
278
+ annotations: {
279
+ title: 'Create Perplexity Computer Handoff',
280
+ readOnlyHint: false,
281
+ openWorldHint: false,
282
+ },
283
+ }, async (args) => {
284
+ const run = createComputerRun({
285
+ task: args.task,
286
+ template: args.template || 'compare',
287
+ opts: { out: args.out, artifactId: args.artifactId },
288
+ config: loadConfig(),
289
+ });
290
+ return toTextResult(run);
291
+ });
292
+
293
+ server.registerTool('pplx_computer_status', {
294
+ title: 'Perplexity Computer Status',
295
+ description: 'Inspect a Perplexity Computer artifact run.',
296
+ inputSchema: {
297
+ run: z.string().min(1).describe('Run id or absolute run folder path.'),
298
+ out: z.string().optional().describe('Artifact root used when run is an id.'),
299
+ },
300
+ annotations: {
301
+ title: 'Perplexity Computer Status',
302
+ readOnlyHint: true,
303
+ openWorldHint: false,
304
+ },
305
+ }, async (args) => {
306
+ const runDir = resolveRunDir(args.run, args.out, loadConfig());
307
+ return toTextResult(inspectComputerRun(runDir));
308
+ });
309
+
310
+ server.registerTool('pplx_computer_import', {
311
+ title: 'Import Perplexity Computer Result',
312
+ description: 'Read and validate a completed computer-result.json from a Perplexity Computer artifact run.',
313
+ inputSchema: {
314
+ run: z.string().min(1).describe('Run id or absolute run folder path.'),
315
+ out: z.string().optional().describe('Artifact root used when run is an id.'),
316
+ },
317
+ annotations: {
318
+ title: 'Import Perplexity Computer Result',
319
+ readOnlyHint: true,
320
+ openWorldHint: false,
321
+ },
322
+ }, async (args) => {
323
+ const runDir = resolveRunDir(args.run, args.out, loadConfig());
324
+ return toTextResult(importComputerResult(runDir));
325
+ });
326
+
327
+ server.registerTool('pplx_computer_read_task', {
328
+ title: 'Read Perplexity Computer Task',
329
+ description: 'Read the task.md prompt from a Perplexity Computer artifact run.',
330
+ inputSchema: {
331
+ run: z.string().min(1).describe('Run id or absolute run folder path.'),
332
+ out: z.string().optional().describe('Artifact root used when run is an id.'),
333
+ },
334
+ annotations: {
335
+ title: 'Read Perplexity Computer Task',
336
+ readOnlyHint: true,
337
+ openWorldHint: false,
338
+ },
339
+ }, async (args) => {
340
+ const runDir = resolveRunDir(args.run, args.out, loadConfig());
341
+ return toTextResult({ runDir, task: readTaskFile(runDir) });
342
+ });
343
+
344
+ return server;
345
+ }
346
+
347
+ export async function runMcpServer() {
348
+ const server = createPplxMcpServer();
349
+ const transport = new StdioServerTransport();
350
+ await server.connect(transport);
351
+ console.error('pplx MCP server running on stdio');
352
+ }