pplx-npx-search 0.3.1 → 0.3.3
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 +14 -0
- package/README.md +51 -0
- package/bin/pplx-mcp.js +7 -0
- package/package.json +6 -3
- package/src/cli.js +104 -1
- package/src/computer.js +58 -5
- package/src/council.js +282 -0
- package/src/mcp-server.js +437 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.3] - 2026-05-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Perplexity Model Council handoff.** Added `pplx council` artifact workflows plus MCP tools for creating, reading, checking, and importing Model Council review runs.
|
|
12
|
+
- **Competitive-analysis template.** Added a `competitive-analysis` Perplexity Computer template that produces evidence briefs with a Council review prompt.
|
|
13
|
+
|
|
14
|
+
## [0.3.2] - 2026-05-26
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **MCP server.** Added `pplx-mcp`, a stdio MCP server for Claude Desktop, Claude Code, Codex, and other MCP clients.
|
|
18
|
+
- **MCP tool surface.** Exposes search, labs, auth status, model listing, and Perplexity Computer artifact tools through MCP.
|
|
19
|
+
|
|
8
20
|
## [0.3.1] - 2026-05-22
|
|
9
21
|
|
|
10
22
|
### Changed
|
|
@@ -67,6 +79,8 @@ First public release worth telling people about. (v0.2.0 was unpublished before
|
|
|
67
79
|
- SSE streaming for real-time answers
|
|
68
80
|
- Optional Playwright and Chrome CDP transports
|
|
69
81
|
|
|
82
|
+
[0.3.3]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.2...v0.3.3
|
|
83
|
+
[0.3.2]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.1...v0.3.2
|
|
70
84
|
[0.3.1]: https://github.com/thatsrajan/pplx-cli/compare/v0.3.0...v0.3.1
|
|
71
85
|
[0.3.0]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.2...v0.3.0
|
|
72
86
|
[0.2.2]: https://github.com/thatsrajan/pplx-cli/compare/v0.2.1...v0.2.2
|
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ pplx reason "explain the Riemann hypothesis"
|
|
|
102
102
|
pplx research "compare React vs Vue in 2026"
|
|
103
103
|
pplx labs "hello world" # free, no auth needed
|
|
104
104
|
pplx computer new "compare dinner options nearby"
|
|
105
|
+
pplx council new "critique the competitive threat from Acme in enterprise search"
|
|
105
106
|
pplx models # list available models
|
|
106
107
|
```
|
|
107
108
|
|
|
@@ -152,6 +153,43 @@ pplx search "research this topic" --json --raw --mode pro
|
|
|
152
153
|
}
|
|
153
154
|
```
|
|
154
155
|
|
|
156
|
+
### MCP Server
|
|
157
|
+
|
|
158
|
+
`pplx-mcp` exposes the same Perplexity workflow through a local stdio MCP server for Claude Desktop, Claude Code, Codex, and other MCP clients.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pplx-mcp
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Typical global client registrations:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
claude mcp add --scope user pplx -- /path/to/node /path/to/pplx-mcp
|
|
168
|
+
codex mcp add pplx -- /path/to/node /path/to/pplx-mcp
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For Claude Desktop, add the server under `mcpServers` in `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"pplx": {
|
|
177
|
+
"command": "/path/to/node",
|
|
178
|
+
"args": ["/path/to/pplx-mcp"]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The MCP server exposes:
|
|
185
|
+
|
|
186
|
+
- `pplx_search`: authenticated `search`, `reasoning`, and `deep-research` queries with sources and artifacts.
|
|
187
|
+
- `pplx_labs`: Labs model queries that do not require cookie auth.
|
|
188
|
+
- `pplx_auth_status`: validates the stored Perplexity browser cookies.
|
|
189
|
+
- `pplx_models`: lists known model aliases.
|
|
190
|
+
- `pplx_computer_create`, `pplx_computer_status`, `pplx_computer_read_task`, and `pplx_computer_import`: Perplexity Computer artifact handoff tools.
|
|
191
|
+
- `pplx_council_create`, `pplx_council_status`, `pplx_council_read_task`, and `pplx_council_import`: Perplexity Model Council artifact handoff tools.
|
|
192
|
+
|
|
155
193
|
---
|
|
156
194
|
|
|
157
195
|
## Artifacts
|
|
@@ -164,6 +202,7 @@ Query-producing commands save artifacts by default:
|
|
|
164
202
|
- `pplx labs`
|
|
165
203
|
- bare `pplx "query"`
|
|
166
204
|
- `pplx computer new`
|
|
205
|
+
- `pplx council new`
|
|
167
206
|
|
|
168
207
|
Each run gets a folder containing `meta.json`, `query.txt`, `answer.md`, `result.json`, and `sources.json`. Use `--out <dir>` to choose the destination for one run, or set `"artifactDir"` in `~/.config/pplx/config.json` to make it persistent. Use `--artifact-id <id>` when an agent needs a deterministic run folder, and `--no-artifact` to disable saving for one search-style run.
|
|
169
208
|
|
|
@@ -184,6 +223,18 @@ pplx computer import <run-id> --out ~/Dropbox/pplx-runs --json
|
|
|
184
223
|
|
|
185
224
|
Computer runs include `task.md`, `result.schema.json`, and `computer-result.json`. Paste `task.md` into Perplexity Computer; when the task is done, place the structured result in `computer-result.json` so local agents can read it.
|
|
186
225
|
|
|
226
|
+
`pplx council` is an artifact handoff for Perplexity Model Council. It requires Model Council access in the Perplexity web UI and creates a task prompt plus result contract without claiming a private Council API:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pplx council new "critique the competitive threat from Acme in enterprise search" --out ~/Dropbox/pplx-runs
|
|
230
|
+
pplx council new "review this evidence" --evidence ~/Dropbox/pplx-runs/acme/computer-result.json
|
|
231
|
+
pplx council open <run-id> --copy
|
|
232
|
+
pplx council status <run-id> --out ~/Dropbox/pplx-runs
|
|
233
|
+
pplx council import <run-id> --out ~/Dropbox/pplx-runs --json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Council runs include `task.md`, `result.schema.json`, and `council-result.json`. Paste `task.md` into Perplexity Model Council; when the review is done, place the structured result in `council-result.json` so local agents can read it.
|
|
237
|
+
|
|
187
238
|
---
|
|
188
239
|
|
|
189
240
|
## Options
|
package/bin/pplx-mcp.js
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pplx-npx-search",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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.
|
|
48
|
+
"ws": "^8.21.0",
|
|
49
|
+
"zod": "^4.4.3"
|
|
47
50
|
}
|
|
48
51
|
}
|
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { loadConfig } from './config.js';
|
|
|
15
15
|
import { resolveTimeoutMs } from './timeout.js';
|
|
16
16
|
import { makeArtifactContext, resolveArtifactDir, writeStandardArtifact } from './artifacts.js';
|
|
17
17
|
import {
|
|
18
|
+
COMPUTER_TEMPLATES,
|
|
18
19
|
createComputerRun,
|
|
19
20
|
copyTextToClipboard,
|
|
20
21
|
importComputerResult,
|
|
@@ -22,6 +23,14 @@ import {
|
|
|
22
23
|
openComputerUrl,
|
|
23
24
|
readTaskFile,
|
|
24
25
|
} from './computer.js';
|
|
26
|
+
import {
|
|
27
|
+
COUNCIL_TEMPLATES,
|
|
28
|
+
copyCouncilTaskToClipboard,
|
|
29
|
+
createCouncilRun,
|
|
30
|
+
importCouncilResult,
|
|
31
|
+
inspectCouncilRun,
|
|
32
|
+
openCouncilUrl,
|
|
33
|
+
} from './council.js';
|
|
25
34
|
|
|
26
35
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
27
36
|
|
|
@@ -562,7 +571,7 @@ const computer = program
|
|
|
562
571
|
addArtifactOptions(computer
|
|
563
572
|
.command('new [task]')
|
|
564
573
|
.description('Create a Perplexity Computer task artifact')
|
|
565
|
-
.option('--template <name>',
|
|
574
|
+
.option('--template <name>', `Computer task template: ${COMPUTER_TEMPLATES.join(', ')}`, 'compare')
|
|
566
575
|
.option('--json', 'Output run metadata as JSON'), { allowDisable: false })
|
|
567
576
|
.action(async (taskArg, opts) => {
|
|
568
577
|
opts = getOpts(opts);
|
|
@@ -649,6 +658,100 @@ addArtifactOptions(computer
|
|
|
649
658
|
}
|
|
650
659
|
});
|
|
651
660
|
|
|
661
|
+
// Council artifact handoff workflow
|
|
662
|
+
const council = program
|
|
663
|
+
.command('council')
|
|
664
|
+
.description('Create and manage Perplexity Model Council artifact handoffs');
|
|
665
|
+
|
|
666
|
+
addArtifactOptions(council
|
|
667
|
+
.command('new [task]')
|
|
668
|
+
.description('Create a Perplexity Model Council task artifact')
|
|
669
|
+
.option('--template <name>', `Council task template: ${COUNCIL_TEMPLATES.join(', ')}`, 'competitive-analysis')
|
|
670
|
+
.option('--evidence <path>', 'Optional local evidence artifact path to reference in the Council task')
|
|
671
|
+
.option('--json', 'Output run metadata as JSON'), { allowDisable: false })
|
|
672
|
+
.action(async (taskArg, opts) => {
|
|
673
|
+
opts = getOpts(opts);
|
|
674
|
+
const task = await resolveQuery(taskArg);
|
|
675
|
+
try {
|
|
676
|
+
const run = createCouncilRun({
|
|
677
|
+
task,
|
|
678
|
+
template: opts.template,
|
|
679
|
+
evidencePath: opts.evidence,
|
|
680
|
+
opts,
|
|
681
|
+
config: loadConfig(),
|
|
682
|
+
});
|
|
683
|
+
if (opts.json) {
|
|
684
|
+
console.log(JSON.stringify(run));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
console.log(chalk.green(`✓ Council task artifact created: ${run.artifactDir}`));
|
|
688
|
+
console.log(chalk.dim(` Task: ${run.taskPath}`));
|
|
689
|
+
console.log(chalk.dim(` Result: ${run.resultPath}`));
|
|
690
|
+
} catch (e) {
|
|
691
|
+
console.error(chalk.red(e.message));
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
addArtifactOptions(council
|
|
697
|
+
.command('open <run>')
|
|
698
|
+
.description('Open Perplexity and optionally copy task.md for Model Council')
|
|
699
|
+
.option('--copy', 'Copy task.md to the clipboard'), { allowDisable: false })
|
|
700
|
+
.action((runId, opts) => {
|
|
701
|
+
opts = getOpts(opts);
|
|
702
|
+
const runDir = resolveRunDir(runId, opts);
|
|
703
|
+
try {
|
|
704
|
+
if (opts.copy && !copyCouncilTaskToClipboard(runDir)) {
|
|
705
|
+
console.log(chalk.yellow('Clipboard copy is only supported on macOS.'));
|
|
706
|
+
}
|
|
707
|
+
if (!openCouncilUrl()) {
|
|
708
|
+
console.log(chalk.yellow('Opening Perplexity is only supported on macOS.'));
|
|
709
|
+
console.log('https://www.perplexity.ai/');
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
console.log(chalk.green(`✓ Opened Perplexity for ${runDir}`));
|
|
713
|
+
if (opts.copy) console.log(chalk.dim(' Copied task.md to clipboard.'));
|
|
714
|
+
} catch (e) {
|
|
715
|
+
console.error(chalk.red(e.message));
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
addArtifactOptions(council
|
|
721
|
+
.command('status <run>')
|
|
722
|
+
.description('Inspect a Perplexity Model Council artifact run')
|
|
723
|
+
.option('--json', 'Output status as JSON'), { allowDisable: false })
|
|
724
|
+
.action((runId, opts) => {
|
|
725
|
+
opts = getOpts(opts);
|
|
726
|
+
const status = inspectCouncilRun(resolveRunDir(runId, opts));
|
|
727
|
+
if (opts.json) {
|
|
728
|
+
console.log(JSON.stringify(status));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const label = status.status === 'complete'
|
|
732
|
+
? chalk.green('[✓] Complete')
|
|
733
|
+
: status.status === 'pending'
|
|
734
|
+
? chalk.yellow('[○] Pending')
|
|
735
|
+
: chalk.red(status.status === 'invalid' ? '[!] Invalid' : '[✗] Missing');
|
|
736
|
+
console.log(`${label} ${status.artifactDir}`);
|
|
737
|
+
if (status.reason) console.log(chalk.dim(` ${status.reason}`));
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
addArtifactOptions(council
|
|
741
|
+
.command('import <run>')
|
|
742
|
+
.description('Print a completed Perplexity Model Council result')
|
|
743
|
+
.option('--json', 'Output compact JSON'), { allowDisable: false })
|
|
744
|
+
.action((runId, opts) => {
|
|
745
|
+
opts = getOpts(opts);
|
|
746
|
+
try {
|
|
747
|
+
const result = importCouncilResult(resolveRunDir(runId, opts));
|
|
748
|
+
console.log(opts.json ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
749
|
+
} catch (e) {
|
|
750
|
+
console.error(chalk.red(e.message));
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
652
755
|
// Models command
|
|
653
756
|
program
|
|
654
757
|
.command('models')
|
package/src/computer.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
export const COMPUTER_URL = 'https://www.perplexity.ai/computer';
|
|
12
12
|
export const COMPUTER_RESULT_FILE = 'computer-result.json';
|
|
13
|
+
export const COMPUTER_TEMPLATES = ['compare', 'competitive-analysis'];
|
|
13
14
|
export const PENDING_COMPUTER_RESULT = {
|
|
14
15
|
summary: '',
|
|
15
16
|
winner: '',
|
|
@@ -18,6 +19,7 @@ export const PENDING_COMPUTER_RESULT = {
|
|
|
18
19
|
sources: [],
|
|
19
20
|
checked_at: '',
|
|
20
21
|
notes: [],
|
|
22
|
+
council_review_prompt: '',
|
|
21
23
|
_status: 'pending',
|
|
22
24
|
};
|
|
23
25
|
|
|
@@ -34,14 +36,11 @@ const RESULT_SCHEMA = {
|
|
|
34
36
|
sources: { type: 'array' },
|
|
35
37
|
checked_at: { type: 'string' },
|
|
36
38
|
notes: { type: 'array' },
|
|
39
|
+
council_review_prompt: { type: 'string' },
|
|
37
40
|
},
|
|
38
41
|
};
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
if (template !== 'compare') {
|
|
42
|
-
throw new Error(`unsupported computer template: ${template}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
43
|
+
function buildCompareTask({ task, resultPath }) {
|
|
45
44
|
return `# Perplexity Computer Task
|
|
46
45
|
|
|
47
46
|
You are running a live comparison task for a local agent workflow.
|
|
@@ -72,6 +71,60 @@ If you cannot access the filesystem, return the JSON in the chat so the local ag
|
|
|
72
71
|
`;
|
|
73
72
|
}
|
|
74
73
|
|
|
74
|
+
function buildCompetitiveAnalysisTask({ task, resultPath }) {
|
|
75
|
+
return `# Perplexity Computer Task
|
|
76
|
+
|
|
77
|
+
You are running a live competitive-analysis task for a local agent workflow.
|
|
78
|
+
|
|
79
|
+
## User request
|
|
80
|
+
|
|
81
|
+
${task}
|
|
82
|
+
|
|
83
|
+
## Instructions
|
|
84
|
+
|
|
85
|
+
- Treat this as "one competitor or competitor set" plus "one topic, market, product surface, capability, pricing motion, GTM motion, or customer segment."
|
|
86
|
+
- Use live web pages and direct source pages where possible, not only search snippets.
|
|
87
|
+
- Check competitor-owned pages first: home page, product docs, pricing, changelog, blog, help center, release notes, status pages, public roadmaps, job postings, and app marketplace listings.
|
|
88
|
+
- Add independent evidence where useful: customer reviews, forum threads, analyst commentary, news, benchmark posts, partner pages, search ads, and social posts.
|
|
89
|
+
- Preserve source URLs for every material claim.
|
|
90
|
+
- Separate facts from interpretation.
|
|
91
|
+
- Identify:
|
|
92
|
+
- current positioning
|
|
93
|
+
- recent changes or launches
|
|
94
|
+
- pricing and packaging implications
|
|
95
|
+
- GTM and distribution signals
|
|
96
|
+
- customer pain or praise signals
|
|
97
|
+
- likely strategic intent
|
|
98
|
+
- threat level to the user request's context
|
|
99
|
+
- evidence gaps that need follow-up
|
|
100
|
+
- Include the time the information was checked.
|
|
101
|
+
- Mark uncertainty explicitly. Do not invent missing values.
|
|
102
|
+
- Prefer structured evidence over prose.
|
|
103
|
+
- Include \`council_review_prompt\`: a concise prompt that can be pasted into Perplexity Model Council to critique the evidence and strategy read.
|
|
104
|
+
|
|
105
|
+
## Output target
|
|
106
|
+
|
|
107
|
+
Write the final result as JSON matching \`result.schema.json\`.
|
|
108
|
+
|
|
109
|
+
If you can access the local filesystem, save it here:
|
|
110
|
+
|
|
111
|
+
\`${resultPath}\`
|
|
112
|
+
|
|
113
|
+
If you cannot access the filesystem, return the JSON in the chat so the local agent or user can place it in that file.
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildComputerTask({ task, template = 'compare', resultPath }) {
|
|
118
|
+
if (!COMPUTER_TEMPLATES.includes(template)) {
|
|
119
|
+
throw new Error(`unsupported computer template: ${template}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (template === 'competitive-analysis') {
|
|
123
|
+
return buildCompetitiveAnalysisTask({ task, resultPath });
|
|
124
|
+
}
|
|
125
|
+
return buildCompareTask({ task, resultPath });
|
|
126
|
+
}
|
|
127
|
+
|
|
75
128
|
export function createComputerRun({ task, template = 'compare', opts = {}, config = {} }) {
|
|
76
129
|
const ctx = makeArtifactContext({ command: 'computer', query: task, opts, config });
|
|
77
130
|
if (!ctx) {
|
package/src/council.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
ARTIFACT_SCHEMA_VERSION,
|
|
6
|
+
makeArtifactContext,
|
|
7
|
+
readJsonFile,
|
|
8
|
+
writeJson,
|
|
9
|
+
} from './artifacts.js';
|
|
10
|
+
import { copyTextToClipboard } from './computer.js';
|
|
11
|
+
|
|
12
|
+
export const COUNCIL_URL = 'https://www.perplexity.ai/';
|
|
13
|
+
export const COUNCIL_RESULT_FILE = 'council-result.json';
|
|
14
|
+
export const COUNCIL_TEMPLATES = ['competitive-analysis', 'strategy-review'];
|
|
15
|
+
export const PENDING_COUNCIL_RESULT = {
|
|
16
|
+
summary: '',
|
|
17
|
+
consensus: [],
|
|
18
|
+
disagreements: [],
|
|
19
|
+
risks: [],
|
|
20
|
+
recommendations: [],
|
|
21
|
+
followups: [],
|
|
22
|
+
sources: [],
|
|
23
|
+
confidence: 'low',
|
|
24
|
+
checked_at: '',
|
|
25
|
+
notes: [],
|
|
26
|
+
_status: 'pending',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const RESULT_SCHEMA = {
|
|
30
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
31
|
+
title: 'pplx council review result',
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: [
|
|
34
|
+
'summary',
|
|
35
|
+
'consensus',
|
|
36
|
+
'disagreements',
|
|
37
|
+
'risks',
|
|
38
|
+
'recommendations',
|
|
39
|
+
'followups',
|
|
40
|
+
'sources',
|
|
41
|
+
'confidence',
|
|
42
|
+
'checked_at',
|
|
43
|
+
'notes',
|
|
44
|
+
],
|
|
45
|
+
properties: {
|
|
46
|
+
summary: { type: 'string' },
|
|
47
|
+
consensus: { type: 'array' },
|
|
48
|
+
disagreements: { type: 'array' },
|
|
49
|
+
risks: { type: 'array' },
|
|
50
|
+
recommendations: { type: 'array' },
|
|
51
|
+
followups: { type: 'array' },
|
|
52
|
+
sources: { type: 'array' },
|
|
53
|
+
confidence: { enum: ['low', 'medium', 'high'] },
|
|
54
|
+
checked_at: { type: 'string' },
|
|
55
|
+
notes: { type: 'array' },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function buildCompetitiveAnalysisCouncilTask({ task, evidencePath, resultPath }) {
|
|
60
|
+
const evidenceBlock = evidencePath
|
|
61
|
+
? `\n## Evidence brief\n\nUse this local evidence artifact if available:\n\n\`${evidencePath}\`\n`
|
|
62
|
+
: '';
|
|
63
|
+
|
|
64
|
+
return `# Perplexity Model Council Task
|
|
65
|
+
|
|
66
|
+
Use Perplexity Model Council for a multi-model review. This is a judgment and synthesis pass, not the primary web-browsing pass.
|
|
67
|
+
|
|
68
|
+
## User request
|
|
69
|
+
|
|
70
|
+
${task}
|
|
71
|
+
${evidenceBlock}
|
|
72
|
+
## Instructions
|
|
73
|
+
|
|
74
|
+
- Ask the Council to evaluate the same evidence from multiple model perspectives before synthesizing.
|
|
75
|
+
- Focus on the competitor, topic, market, product capability, pricing motion, GTM motion, or customer segment named in the request.
|
|
76
|
+
- Separate source-backed facts from strategic interpretation.
|
|
77
|
+
- Identify where the models agree and where they disagree.
|
|
78
|
+
- Challenge the strongest assumption in the analysis.
|
|
79
|
+
- Surface missing evidence that would change the conclusion.
|
|
80
|
+
- Produce concrete recommendations and monitoring follow-ups.
|
|
81
|
+
- Preserve source URLs from the evidence brief and add any new source URLs used by Council.
|
|
82
|
+
- Mark uncertainty explicitly. Do not invent missing values.
|
|
83
|
+
|
|
84
|
+
## Output target
|
|
85
|
+
|
|
86
|
+
Return the final result as JSON matching \`result.schema.json\`.
|
|
87
|
+
|
|
88
|
+
If you can access the local filesystem, save it here:
|
|
89
|
+
|
|
90
|
+
\`${resultPath}\`
|
|
91
|
+
|
|
92
|
+
If you cannot access the filesystem, return the JSON in the chat so the local agent or user can place it in that file.
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildStrategyReviewCouncilTask({ task, evidencePath, resultPath }) {
|
|
97
|
+
const evidenceBlock = evidencePath
|
|
98
|
+
? `\n## Evidence brief\n\nUse this local evidence artifact if available:\n\n\`${evidencePath}\`\n`
|
|
99
|
+
: '';
|
|
100
|
+
|
|
101
|
+
return `# Perplexity Model Council Task
|
|
102
|
+
|
|
103
|
+
Use Perplexity Model Council for a multi-model strategy review.
|
|
104
|
+
|
|
105
|
+
## User request
|
|
106
|
+
|
|
107
|
+
${task}
|
|
108
|
+
${evidenceBlock}
|
|
109
|
+
## Instructions
|
|
110
|
+
|
|
111
|
+
- Ask the Council to evaluate the request from multiple reasoning perspectives before synthesizing.
|
|
112
|
+
- Identify consensus, disagreements, risks, blind spots, and decision criteria.
|
|
113
|
+
- Separate source-backed facts from interpretation.
|
|
114
|
+
- Challenge the strongest assumption.
|
|
115
|
+
- Surface missing evidence that would change the conclusion.
|
|
116
|
+
- Produce concrete recommendations and follow-up questions.
|
|
117
|
+
- Preserve source URLs from the evidence brief and add any new source URLs used by Council.
|
|
118
|
+
- Mark uncertainty explicitly. Do not invent missing values.
|
|
119
|
+
|
|
120
|
+
## Output target
|
|
121
|
+
|
|
122
|
+
Return the final result as JSON matching \`result.schema.json\`.
|
|
123
|
+
|
|
124
|
+
If you can access the local filesystem, save it here:
|
|
125
|
+
|
|
126
|
+
\`${resultPath}\`
|
|
127
|
+
|
|
128
|
+
If you cannot access the filesystem, return the JSON in the chat so the local agent or user can place it in that file.
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildCouncilTask({
|
|
133
|
+
task,
|
|
134
|
+
template = 'competitive-analysis',
|
|
135
|
+
evidencePath,
|
|
136
|
+
resultPath,
|
|
137
|
+
}) {
|
|
138
|
+
if (!COUNCIL_TEMPLATES.includes(template)) {
|
|
139
|
+
throw new Error(`unsupported council template: ${template}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (template === 'strategy-review') {
|
|
143
|
+
return buildStrategyReviewCouncilTask({ task, evidencePath, resultPath });
|
|
144
|
+
}
|
|
145
|
+
return buildCompetitiveAnalysisCouncilTask({ task, evidencePath, resultPath });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createCouncilRun({
|
|
149
|
+
task,
|
|
150
|
+
template = 'competitive-analysis',
|
|
151
|
+
evidencePath,
|
|
152
|
+
opts = {},
|
|
153
|
+
config = {},
|
|
154
|
+
}) {
|
|
155
|
+
const ctx = makeArtifactContext({ command: 'council', query: task, opts, config });
|
|
156
|
+
if (!ctx) {
|
|
157
|
+
throw new Error('council runs require artifacts; omit --no-artifact for this command');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
mkdirSync(ctx.artifactDir, { recursive: true });
|
|
161
|
+
const createdAt = new Date().toISOString();
|
|
162
|
+
const resultPath = join(ctx.artifactDir, COUNCIL_RESULT_FILE);
|
|
163
|
+
const taskText = buildCouncilTask({ task, template, evidencePath, resultPath });
|
|
164
|
+
const meta = {
|
|
165
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
166
|
+
command: 'council',
|
|
167
|
+
query: task,
|
|
168
|
+
template,
|
|
169
|
+
evidencePath: evidencePath || null,
|
|
170
|
+
artifactId: ctx.artifactId,
|
|
171
|
+
artifactDir: ctx.artifactDir,
|
|
172
|
+
createdAt,
|
|
173
|
+
status: 'pending',
|
|
174
|
+
};
|
|
175
|
+
const result = {
|
|
176
|
+
query: task,
|
|
177
|
+
answer: '',
|
|
178
|
+
sources: [],
|
|
179
|
+
command: 'council',
|
|
180
|
+
mode: 'council',
|
|
181
|
+
model: 'model-council',
|
|
182
|
+
template,
|
|
183
|
+
evidencePath: evidencePath || null,
|
|
184
|
+
artifactId: ctx.artifactId,
|
|
185
|
+
artifactDir: ctx.artifactDir,
|
|
186
|
+
createdAt,
|
|
187
|
+
status: 'pending',
|
|
188
|
+
councilResultFile: COUNCIL_RESULT_FILE,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
writeJson(join(ctx.artifactDir, 'meta.json'), meta);
|
|
192
|
+
writeFileSync(join(ctx.artifactDir, 'query.txt'), `${task}\n`, 'utf8');
|
|
193
|
+
writeFileSync(join(ctx.artifactDir, 'answer.md'), taskText, 'utf8');
|
|
194
|
+
writeJson(join(ctx.artifactDir, 'result.json'), result);
|
|
195
|
+
writeJson(join(ctx.artifactDir, 'sources.json'), []);
|
|
196
|
+
writeFileSync(join(ctx.artifactDir, 'task.md'), taskText, 'utf8');
|
|
197
|
+
writeJson(join(ctx.artifactDir, 'result.schema.json'), RESULT_SCHEMA);
|
|
198
|
+
writeJson(join(ctx.artifactDir, COUNCIL_RESULT_FILE), PENDING_COUNCIL_RESULT);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
artifactId: ctx.artifactId,
|
|
202
|
+
artifactDir: ctx.artifactDir,
|
|
203
|
+
taskPath: join(ctx.artifactDir, 'task.md'),
|
|
204
|
+
resultPath,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertCouncilResult(value) {
|
|
209
|
+
const missing = [
|
|
210
|
+
'summary',
|
|
211
|
+
'consensus',
|
|
212
|
+
'disagreements',
|
|
213
|
+
'risks',
|
|
214
|
+
'recommendations',
|
|
215
|
+
'followups',
|
|
216
|
+
'sources',
|
|
217
|
+
'confidence',
|
|
218
|
+
'checked_at',
|
|
219
|
+
'notes',
|
|
220
|
+
].filter((key) => !(key in value));
|
|
221
|
+
if (missing.length) return { ok: false, reason: `missing fields: ${missing.join(', ')}` };
|
|
222
|
+
if (!['low', 'medium', 'high'].includes(value.confidence)) {
|
|
223
|
+
return { ok: false, reason: 'confidence must be low, medium, or high' };
|
|
224
|
+
}
|
|
225
|
+
for (const key of ['consensus', 'disagreements', 'risks', 'recommendations', 'followups', 'sources', 'notes']) {
|
|
226
|
+
if (!Array.isArray(value[key])) return { ok: false, reason: `${key} must be an array` };
|
|
227
|
+
}
|
|
228
|
+
return { ok: true, reason: null };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function inspectCouncilRun(runDir) {
|
|
232
|
+
const artifactDir = resolve(runDir);
|
|
233
|
+
const resultPath = join(artifactDir, COUNCIL_RESULT_FILE);
|
|
234
|
+
const metaPath = join(artifactDir, 'meta.json');
|
|
235
|
+
if (!existsSync(metaPath)) {
|
|
236
|
+
return { status: 'missing', artifactDir, resultPath, reason: 'meta.json not found' };
|
|
237
|
+
}
|
|
238
|
+
if (!existsSync(resultPath)) {
|
|
239
|
+
return { status: 'pending', artifactDir, resultPath, reason: `${COUNCIL_RESULT_FILE} not found` };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const result = readJsonFile(resultPath);
|
|
243
|
+
if (result._status === 'pending') {
|
|
244
|
+
return { status: 'pending', artifactDir, resultPath, reason: `${COUNCIL_RESULT_FILE} is still pending` };
|
|
245
|
+
}
|
|
246
|
+
const validation = assertCouncilResult(result);
|
|
247
|
+
return {
|
|
248
|
+
status: validation.ok ? 'complete' : 'invalid',
|
|
249
|
+
artifactDir,
|
|
250
|
+
resultPath,
|
|
251
|
+
reason: validation.reason,
|
|
252
|
+
result,
|
|
253
|
+
};
|
|
254
|
+
} catch (e) {
|
|
255
|
+
return { status: 'invalid', artifactDir, resultPath, reason: e.message };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function importCouncilResult(runDir) {
|
|
260
|
+
const status = inspectCouncilRun(runDir);
|
|
261
|
+
if (status.status !== 'complete') {
|
|
262
|
+
throw new Error(`council result is ${status.status}: ${status.reason}`);
|
|
263
|
+
}
|
|
264
|
+
return status.result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function openCouncilUrl() {
|
|
268
|
+
if (process.platform === 'darwin') {
|
|
269
|
+
execFileSync('open', [COUNCIL_URL]);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function copyCouncilTaskToClipboard(runDir) {
|
|
276
|
+
const taskText = readCouncilTaskFile(runDir);
|
|
277
|
+
return copyTextToClipboard(taskText);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function readCouncilTaskFile(runDir) {
|
|
281
|
+
return readFileSync(join(resolve(runDir), 'task.md'), 'utf8');
|
|
282
|
+
}
|
|
@@ -0,0 +1,437 @@
|
|
|
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
|
+
COMPUTER_TEMPLATES,
|
|
15
|
+
createComputerRun,
|
|
16
|
+
importComputerResult,
|
|
17
|
+
inspectComputerRun,
|
|
18
|
+
readTaskFile,
|
|
19
|
+
} from './computer.js';
|
|
20
|
+
import {
|
|
21
|
+
COUNCIL_TEMPLATES,
|
|
22
|
+
createCouncilRun,
|
|
23
|
+
importCouncilResult,
|
|
24
|
+
inspectCouncilRun,
|
|
25
|
+
readCouncilTaskFile,
|
|
26
|
+
} from './council.js';
|
|
27
|
+
import { MODEL_MAP, LABS_MODELS } from './constants.js';
|
|
28
|
+
|
|
29
|
+
const SERVER_NAME = 'pplx';
|
|
30
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
31
|
+
const SERVER_VERSION = pkg.version;
|
|
32
|
+
const SEARCH_MODES = ['auto', 'pro', 'reasoning', 'deep-research'];
|
|
33
|
+
const TRANSPORTS = ['auto', 'http', 'playwright', 'curl', 'chrome'];
|
|
34
|
+
|
|
35
|
+
const optionalArtifactArgs = {
|
|
36
|
+
out: z.string().optional().describe('Artifact root for this run. Defaults to ~/.config/pplx/config.json artifactDir.'),
|
|
37
|
+
artifactId: z.string().optional().describe('Deterministic artifact id for this run.'),
|
|
38
|
+
saveArtifact: z.boolean().optional().default(true).describe('Save the standard pplx artifact files.'),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function toTextResult(value) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
44
|
+
structuredContent: value,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeSources(sources) {
|
|
49
|
+
if (!sources) return ['web'];
|
|
50
|
+
if (Array.isArray(sources)) return sources;
|
|
51
|
+
return String(sources).split(',').map((source) => source.trim()).filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeSourceResult(result) {
|
|
55
|
+
return {
|
|
56
|
+
title: result.name || result.title || '',
|
|
57
|
+
url: result.url || '',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mcpArtifactOpts(args = {}) {
|
|
62
|
+
if (args.saveArtifact === false) return { artifact: false };
|
|
63
|
+
return {
|
|
64
|
+
out: args.out,
|
|
65
|
+
artifactId: args.artifactId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function transportOpts(transport, config) {
|
|
70
|
+
if (transport === 'chrome') return { chrome: true };
|
|
71
|
+
if (transport === 'playwright') return { playwright: true };
|
|
72
|
+
if (transport === 'curl') return { curl: true };
|
|
73
|
+
if (transport === 'http') return { playwright: false, chrome: false, curl: false };
|
|
74
|
+
return {
|
|
75
|
+
chrome: config.chrome,
|
|
76
|
+
playwright: config.playwright,
|
|
77
|
+
curl: config.curl,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function assertAuthReady({ cookies, opts }) {
|
|
82
|
+
if (opts.chrome || opts.allowAnonymous) return;
|
|
83
|
+
if (Object.keys(cookies).length === 0) {
|
|
84
|
+
throw new Error('No Perplexity cookies stored. Run: pplx auth --browser auto');
|
|
85
|
+
}
|
|
86
|
+
const ok = await testAuth(cookies);
|
|
87
|
+
if (!ok) {
|
|
88
|
+
throw new Error('Stored Perplexity cookies are invalid or expired. Run: pplx auth --browser auto');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function runSearchTool(args) {
|
|
93
|
+
const config = loadConfig();
|
|
94
|
+
const mode = args.mode || 'pro';
|
|
95
|
+
const transport = args.transport || 'auto';
|
|
96
|
+
const opts = {
|
|
97
|
+
...config,
|
|
98
|
+
...transportOpts(transport, config),
|
|
99
|
+
mode,
|
|
100
|
+
model: args.model,
|
|
101
|
+
sources: normalizeSources(args.sources),
|
|
102
|
+
language: args.language || args.lang || 'en-US',
|
|
103
|
+
incognito: args.incognito ?? false,
|
|
104
|
+
allowAnonymous: args.allowAnonymous ?? false,
|
|
105
|
+
timeoutMs: undefined,
|
|
106
|
+
};
|
|
107
|
+
opts.timeoutMs = resolveTimeoutMs({ ...opts, timeoutMs: args.timeoutMs, mode });
|
|
108
|
+
|
|
109
|
+
const cookies = loadCookies() || {};
|
|
110
|
+
await assertAuthReady({ cookies, opts });
|
|
111
|
+
|
|
112
|
+
const artifactCtx = makeArtifactContext({
|
|
113
|
+
command: mode === 'deep-research' ? 'research' : mode === 'reasoning' ? 'reason' : 'search',
|
|
114
|
+
query: args.query,
|
|
115
|
+
opts: mcpArtifactOpts(args),
|
|
116
|
+
config,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let lastAnswer = '';
|
|
120
|
+
let lastData = null;
|
|
121
|
+
for await (const data of search(args.query, cookies, opts)) {
|
|
122
|
+
lastData = data;
|
|
123
|
+
if ((data.answer || '').length >= lastAnswer.length) {
|
|
124
|
+
lastAnswer = data.answer || lastAnswer;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const answer = lastData?.answer || lastAnswer || '';
|
|
129
|
+
if (!answer) throw new Error('No answer received from Perplexity.');
|
|
130
|
+
|
|
131
|
+
const sources = (lastData?.web_results || []).map(normalizeSourceResult);
|
|
132
|
+
const artifactInfo = writeStandardArtifact(artifactCtx, {
|
|
133
|
+
answer,
|
|
134
|
+
sources,
|
|
135
|
+
mode,
|
|
136
|
+
model: args.model || 'default',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
query: args.query,
|
|
141
|
+
answer,
|
|
142
|
+
sources,
|
|
143
|
+
mode,
|
|
144
|
+
model: args.model || 'default',
|
|
145
|
+
artifactDir: artifactInfo?.artifactDir || null,
|
|
146
|
+
artifactId: artifactInfo?.artifactId || null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function runLabsTool(args) {
|
|
151
|
+
const config = loadConfig();
|
|
152
|
+
const model = args.model || 'sonar';
|
|
153
|
+
const artifactCtx = makeArtifactContext({
|
|
154
|
+
command: 'labs',
|
|
155
|
+
query: args.query,
|
|
156
|
+
opts: mcpArtifactOpts(args),
|
|
157
|
+
config,
|
|
158
|
+
});
|
|
159
|
+
const client = new LabsClient();
|
|
160
|
+
let answer = '';
|
|
161
|
+
const events = [];
|
|
162
|
+
try {
|
|
163
|
+
await client.connect();
|
|
164
|
+
for await (const data of client.ask(args.query, model)) {
|
|
165
|
+
events.push(data);
|
|
166
|
+
answer = data.output || answer;
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
client.close();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const artifactInfo = writeStandardArtifact(artifactCtx, {
|
|
173
|
+
answer,
|
|
174
|
+
sources: [],
|
|
175
|
+
mode: 'labs',
|
|
176
|
+
model,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
query: args.query,
|
|
181
|
+
answer,
|
|
182
|
+
events,
|
|
183
|
+
mode: 'labs',
|
|
184
|
+
model,
|
|
185
|
+
artifactDir: artifactInfo?.artifactDir || null,
|
|
186
|
+
artifactId: artifactInfo?.artifactId || null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function getAuthStatus() {
|
|
191
|
+
const cookies = loadCookies() || {};
|
|
192
|
+
const cookieCount = Object.keys(cookies).length;
|
|
193
|
+
if (cookieCount === 0) {
|
|
194
|
+
return {
|
|
195
|
+
authenticated: false,
|
|
196
|
+
cookieCount,
|
|
197
|
+
message: 'No cookies stored. Run: pplx auth --browser auto',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const authenticated = await testAuth(cookies);
|
|
201
|
+
return {
|
|
202
|
+
authenticated,
|
|
203
|
+
cookieCount,
|
|
204
|
+
message: authenticated
|
|
205
|
+
? 'Stored Perplexity cookies are valid.'
|
|
206
|
+
: 'Stored Perplexity cookies are invalid or expired. Run: pplx auth --browser auto',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveRunDir(run, out, config) {
|
|
211
|
+
if (isAbsolute(run) || run.includes('/')) return resolve(run);
|
|
212
|
+
return join(resolveArtifactDir({ out, config }), run);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function createPplxMcpServer() {
|
|
216
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
217
|
+
|
|
218
|
+
server.registerTool('pplx_search', {
|
|
219
|
+
title: 'Perplexity Search',
|
|
220
|
+
description: 'Run an authenticated Perplexity search/reasoning/deep-research query and return answer, sources, and artifact paths.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
query: z.string().min(1).describe('Question or research prompt.'),
|
|
223
|
+
mode: z.enum(SEARCH_MODES).optional().default('pro').describe('Perplexity mode.'),
|
|
224
|
+
model: z.string().optional().describe('Optional model alias or raw Perplexity model id.'),
|
|
225
|
+
sources: z.array(z.string()).optional().default(['web']).describe('Source types such as web, scholar, or social.'),
|
|
226
|
+
language: z.string().optional().default('en-US').describe('Language code.'),
|
|
227
|
+
incognito: z.boolean().optional().default(false).describe('Do not save the query to Perplexity history.'),
|
|
228
|
+
transport: z.enum(TRANSPORTS).optional().default('auto').describe('Transport override for Perplexity calls.'),
|
|
229
|
+
timeoutMs: z.union([z.string(), z.number()]).optional().describe('Overall stream timeout, e.g. 120s, 10m, or milliseconds.'),
|
|
230
|
+
allowAnonymous: z.boolean().optional().default(false).describe('Allow anonymous Perplexity responses when cookies are missing or expired.'),
|
|
231
|
+
...optionalArtifactArgs,
|
|
232
|
+
},
|
|
233
|
+
annotations: {
|
|
234
|
+
title: 'Perplexity Search',
|
|
235
|
+
readOnlyHint: false,
|
|
236
|
+
openWorldHint: true,
|
|
237
|
+
},
|
|
238
|
+
}, async (args) => toTextResult(await runSearchTool(args)));
|
|
239
|
+
|
|
240
|
+
server.registerTool('pplx_labs', {
|
|
241
|
+
title: 'Perplexity Labs',
|
|
242
|
+
description: 'Query Perplexity Labs models without browser-cookie auth.',
|
|
243
|
+
inputSchema: {
|
|
244
|
+
query: z.string().min(1).describe('Question or prompt.'),
|
|
245
|
+
model: z.enum(LABS_MODELS).optional().default('sonar').describe('Labs model.'),
|
|
246
|
+
...optionalArtifactArgs,
|
|
247
|
+
},
|
|
248
|
+
annotations: {
|
|
249
|
+
title: 'Perplexity Labs',
|
|
250
|
+
readOnlyHint: false,
|
|
251
|
+
openWorldHint: true,
|
|
252
|
+
},
|
|
253
|
+
}, async (args) => toTextResult(await runLabsTool(args)));
|
|
254
|
+
|
|
255
|
+
server.registerTool('pplx_auth_status', {
|
|
256
|
+
title: 'Perplexity Auth Status',
|
|
257
|
+
description: 'Check whether stored Perplexity cookies are present and authenticated.',
|
|
258
|
+
inputSchema: {},
|
|
259
|
+
annotations: {
|
|
260
|
+
title: 'Perplexity Auth Status',
|
|
261
|
+
readOnlyHint: true,
|
|
262
|
+
openWorldHint: false,
|
|
263
|
+
},
|
|
264
|
+
}, async () => toTextResult(await getAuthStatus()));
|
|
265
|
+
|
|
266
|
+
server.registerTool('pplx_models', {
|
|
267
|
+
title: 'Perplexity Models',
|
|
268
|
+
description: 'List known Perplexity model aliases exposed by pplx-cli.',
|
|
269
|
+
inputSchema: {},
|
|
270
|
+
annotations: {
|
|
271
|
+
title: 'Perplexity Models',
|
|
272
|
+
readOnlyHint: true,
|
|
273
|
+
openWorldHint: false,
|
|
274
|
+
},
|
|
275
|
+
}, async () => toTextResult({ modes: MODEL_MAP, labs: LABS_MODELS }));
|
|
276
|
+
|
|
277
|
+
server.registerTool('pplx_computer_create', {
|
|
278
|
+
title: 'Create Perplexity Computer Handoff',
|
|
279
|
+
description: 'Create a Perplexity Computer artifact handoff folder containing task.md, result.schema.json, and computer-result.json.',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
task: z.string().min(1).describe('Live web task for Perplexity Computer.'),
|
|
282
|
+
template: z.enum(COMPUTER_TEMPLATES).optional().default('compare').describe('Computer task template.'),
|
|
283
|
+
out: z.string().optional().describe('Artifact root for this run.'),
|
|
284
|
+
artifactId: z.string().optional().describe('Deterministic artifact id for this run.'),
|
|
285
|
+
},
|
|
286
|
+
annotations: {
|
|
287
|
+
title: 'Create Perplexity Computer Handoff',
|
|
288
|
+
readOnlyHint: false,
|
|
289
|
+
openWorldHint: false,
|
|
290
|
+
},
|
|
291
|
+
}, async (args) => {
|
|
292
|
+
const run = createComputerRun({
|
|
293
|
+
task: args.task,
|
|
294
|
+
template: args.template || 'compare',
|
|
295
|
+
opts: { out: args.out, artifactId: args.artifactId },
|
|
296
|
+
config: loadConfig(),
|
|
297
|
+
});
|
|
298
|
+
return toTextResult(run);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
server.registerTool('pplx_council_create', {
|
|
302
|
+
title: 'Create Perplexity Model Council Handoff',
|
|
303
|
+
description: 'Create a Perplexity Model Council artifact handoff folder containing task.md, result.schema.json, and council-result.json.',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
task: z.string().min(1).describe('Judgment, strategy, or competitive-analysis task for Model Council.'),
|
|
306
|
+
template: z.enum(COUNCIL_TEMPLATES).optional().default('competitive-analysis').describe('Council task template.'),
|
|
307
|
+
evidencePath: z.string().optional().describe('Optional local evidence artifact path to reference in the Council task.'),
|
|
308
|
+
out: z.string().optional().describe('Artifact root for this run.'),
|
|
309
|
+
artifactId: z.string().optional().describe('Deterministic artifact id for this run.'),
|
|
310
|
+
},
|
|
311
|
+
annotations: {
|
|
312
|
+
title: 'Create Perplexity Model Council Handoff',
|
|
313
|
+
readOnlyHint: false,
|
|
314
|
+
openWorldHint: false,
|
|
315
|
+
},
|
|
316
|
+
}, async (args) => {
|
|
317
|
+
const run = createCouncilRun({
|
|
318
|
+
task: args.task,
|
|
319
|
+
template: args.template || 'competitive-analysis',
|
|
320
|
+
evidencePath: args.evidencePath,
|
|
321
|
+
opts: { out: args.out, artifactId: args.artifactId },
|
|
322
|
+
config: loadConfig(),
|
|
323
|
+
});
|
|
324
|
+
return toTextResult(run);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
server.registerTool('pplx_council_status', {
|
|
328
|
+
title: 'Perplexity Model Council Status',
|
|
329
|
+
description: 'Inspect a Perplexity Model Council 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: 'Perplexity Model Council Status',
|
|
336
|
+
readOnlyHint: true,
|
|
337
|
+
openWorldHint: false,
|
|
338
|
+
},
|
|
339
|
+
}, async (args) => {
|
|
340
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
341
|
+
return toTextResult(inspectCouncilRun(runDir));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
server.registerTool('pplx_council_import', {
|
|
345
|
+
title: 'Import Perplexity Model Council Result',
|
|
346
|
+
description: 'Read and validate a completed council-result.json from a Perplexity Model Council artifact run.',
|
|
347
|
+
inputSchema: {
|
|
348
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
349
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
350
|
+
},
|
|
351
|
+
annotations: {
|
|
352
|
+
title: 'Import Perplexity Model Council Result',
|
|
353
|
+
readOnlyHint: true,
|
|
354
|
+
openWorldHint: false,
|
|
355
|
+
},
|
|
356
|
+
}, async (args) => {
|
|
357
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
358
|
+
return toTextResult(importCouncilResult(runDir));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
server.registerTool('pplx_council_read_task', {
|
|
362
|
+
title: 'Read Perplexity Model Council Task',
|
|
363
|
+
description: 'Read the task.md prompt from a Perplexity Model Council artifact run.',
|
|
364
|
+
inputSchema: {
|
|
365
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
366
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
367
|
+
},
|
|
368
|
+
annotations: {
|
|
369
|
+
title: 'Read Perplexity Model Council Task',
|
|
370
|
+
readOnlyHint: true,
|
|
371
|
+
openWorldHint: false,
|
|
372
|
+
},
|
|
373
|
+
}, async (args) => {
|
|
374
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
375
|
+
return toTextResult({ runDir, task: readCouncilTaskFile(runDir) });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
server.registerTool('pplx_computer_status', {
|
|
379
|
+
title: 'Perplexity Computer Status',
|
|
380
|
+
description: 'Inspect a Perplexity Computer artifact run.',
|
|
381
|
+
inputSchema: {
|
|
382
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
383
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
384
|
+
},
|
|
385
|
+
annotations: {
|
|
386
|
+
title: 'Perplexity Computer Status',
|
|
387
|
+
readOnlyHint: true,
|
|
388
|
+
openWorldHint: false,
|
|
389
|
+
},
|
|
390
|
+
}, async (args) => {
|
|
391
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
392
|
+
return toTextResult(inspectComputerRun(runDir));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
server.registerTool('pplx_computer_import', {
|
|
396
|
+
title: 'Import Perplexity Computer Result',
|
|
397
|
+
description: 'Read and validate a completed computer-result.json from a Perplexity Computer artifact run.',
|
|
398
|
+
inputSchema: {
|
|
399
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
400
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
401
|
+
},
|
|
402
|
+
annotations: {
|
|
403
|
+
title: 'Import Perplexity Computer Result',
|
|
404
|
+
readOnlyHint: true,
|
|
405
|
+
openWorldHint: false,
|
|
406
|
+
},
|
|
407
|
+
}, async (args) => {
|
|
408
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
409
|
+
return toTextResult(importComputerResult(runDir));
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
server.registerTool('pplx_computer_read_task', {
|
|
413
|
+
title: 'Read Perplexity Computer Task',
|
|
414
|
+
description: 'Read the task.md prompt from a Perplexity Computer artifact run.',
|
|
415
|
+
inputSchema: {
|
|
416
|
+
run: z.string().min(1).describe('Run id or absolute run folder path.'),
|
|
417
|
+
out: z.string().optional().describe('Artifact root used when run is an id.'),
|
|
418
|
+
},
|
|
419
|
+
annotations: {
|
|
420
|
+
title: 'Read Perplexity Computer Task',
|
|
421
|
+
readOnlyHint: true,
|
|
422
|
+
openWorldHint: false,
|
|
423
|
+
},
|
|
424
|
+
}, async (args) => {
|
|
425
|
+
const runDir = resolveRunDir(args.run, args.out, loadConfig());
|
|
426
|
+
return toTextResult({ runDir, task: readTaskFile(runDir) });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return server;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function runMcpServer() {
|
|
433
|
+
const server = createPplxMcpServer();
|
|
434
|
+
const transport = new StdioServerTransport();
|
|
435
|
+
await server.connect(transport);
|
|
436
|
+
console.error('pplx MCP server running on stdio');
|
|
437
|
+
}
|