perplexity-web-mcp-cli 0.14.2__tar.gz → 0.14.4__tar.gz
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.
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/PKG-INFO +10 -2
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/README.md +8 -1
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/pyproject.toml +2 -1
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/api/server.py +2 -2
- perplexity_web_mcp_cli-0.14.4/src/perplexity_web_mcp/auth.py +153 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/ai_doc.py +6 -3
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/auth.py +53 -64
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/setup.py +6 -1
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/constants.py +7 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/core.py +6 -4
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/data/SKILL.md +8 -4
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/data/references/mcp-tools.md +4 -1
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/mcp/server.py +31 -60
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/rate_limits.py +14 -2
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/shared.py +6 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/__init__.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/api/__init__.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/api/responses.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/api/session_manager.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/api/tool_calling.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/__init__.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/doctor.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/hack.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/main.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/skill.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/config.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/council.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/data/references/api-endpoints.md +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/data/references/models.md +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/enums.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/exceptions.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/http.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/limits.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/logging.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/mcp/__init__.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/mcp/__main__.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/models.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/py.typed +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/resilience.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/router.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/sessions.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/token_store.py +0 -0
- {perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: perplexity-web-mcp-cli
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.4
|
|
4
4
|
Summary: CLI, MCP server, and Anthropic/OpenAI API-compatible interface for Perplexity AI.
|
|
5
5
|
Keywords: perplexity,ai,mcp,anthropic,api,client
|
|
6
6
|
Author: Jacob BD
|
|
@@ -22,6 +22,7 @@ Requires-Dist: loguru>=0.7.3,<1.0
|
|
|
22
22
|
Requires-Dist: orjson>=3.11.6,<4.0
|
|
23
23
|
Requires-Dist: pydantic>=2.12.5,<3.0
|
|
24
24
|
Requires-Dist: tenacity>=9.1.2,<10.0
|
|
25
|
+
Requires-Dist: tomli>=2.0.0,<3.0 ; python_full_version < '3.11'
|
|
25
26
|
Requires-Dist: python-dotenv>=1.0.0,<2.0
|
|
26
27
|
Requires-Dist: rich>=13.0.0,<15.0
|
|
27
28
|
Requires-Dist: rich-click>=1.8
|
|
@@ -269,8 +270,12 @@ pwm login # Interactive login (email + OTP)
|
|
|
269
270
|
pwm login --check # Check if authenticated
|
|
270
271
|
pwm login --email user@example.com # Send verification code (non-interactive)
|
|
271
272
|
pwm login --email user@example.com --code 123456 # Complete auth with code
|
|
273
|
+
pwm login --email user@example.com --code 123456 --totp-code 654321 # Account with TOTP enabled
|
|
272
274
|
```
|
|
273
275
|
|
|
276
|
+
Set `PWM_SAVE_TO_LIBRARY=1` to save shared CLI and MCP queries to the Perplexity thread library. Queries are
|
|
277
|
+
incognito by default.
|
|
278
|
+
|
|
274
279
|
### Usage & Limits
|
|
275
280
|
|
|
276
281
|
```bash
|
|
@@ -455,7 +460,7 @@ claude mcp add perplexity pwm-mcp
|
|
|
455
460
|
| `pplx_connectors` | List connector source IDs |
|
|
456
461
|
| `pplx_auth_status` | Check authentication status |
|
|
457
462
|
| `pplx_auth_request_code` | Send verification code to email |
|
|
458
|
-
| `pplx_auth_complete` | Complete
|
|
463
|
+
| `pplx_auth_complete` | Complete email and TOTP authentication |
|
|
459
464
|
|
|
460
465
|
All query tools support `source_focus`: `none`, `web`, `academic`, `social`, `finance`, `all`, or a connector source ID returned by `pplx_connectors()`.
|
|
461
466
|
|
|
@@ -586,6 +591,9 @@ pwm login --email your@email.com --code 123456
|
|
|
586
591
|
2. Check email for 6-digit code
|
|
587
592
|
3. Call `pplx_auth_complete(email="your@email.com", code="123456")`
|
|
588
593
|
|
|
594
|
+
If the tool returns `TOTP_REQUIRED`, call it again with
|
|
595
|
+
`pplx_auth_complete(email="your@email.com", totp_code="654321")`.
|
|
596
|
+
|
|
589
597
|
### Diagnose Issues
|
|
590
598
|
|
|
591
599
|
```bash
|
|
@@ -232,8 +232,12 @@ pwm login # Interactive login (email + OTP)
|
|
|
232
232
|
pwm login --check # Check if authenticated
|
|
233
233
|
pwm login --email user@example.com # Send verification code (non-interactive)
|
|
234
234
|
pwm login --email user@example.com --code 123456 # Complete auth with code
|
|
235
|
+
pwm login --email user@example.com --code 123456 --totp-code 654321 # Account with TOTP enabled
|
|
235
236
|
```
|
|
236
237
|
|
|
238
|
+
Set `PWM_SAVE_TO_LIBRARY=1` to save shared CLI and MCP queries to the Perplexity thread library. Queries are
|
|
239
|
+
incognito by default.
|
|
240
|
+
|
|
237
241
|
### Usage & Limits
|
|
238
242
|
|
|
239
243
|
```bash
|
|
@@ -418,7 +422,7 @@ claude mcp add perplexity pwm-mcp
|
|
|
418
422
|
| `pplx_connectors` | List connector source IDs |
|
|
419
423
|
| `pplx_auth_status` | Check authentication status |
|
|
420
424
|
| `pplx_auth_request_code` | Send verification code to email |
|
|
421
|
-
| `pplx_auth_complete` | Complete
|
|
425
|
+
| `pplx_auth_complete` | Complete email and TOTP authentication |
|
|
422
426
|
|
|
423
427
|
All query tools support `source_focus`: `none`, `web`, `academic`, `social`, `finance`, `all`, or a connector source ID returned by `pplx_connectors()`.
|
|
424
428
|
|
|
@@ -549,6 +553,9 @@ pwm login --email your@email.com --code 123456
|
|
|
549
553
|
2. Check email for 6-digit code
|
|
550
554
|
3. Call `pplx_auth_complete(email="your@email.com", code="123456")`
|
|
551
555
|
|
|
556
|
+
If the tool returns `TOTP_REQUIRED`, call it again with
|
|
557
|
+
`pplx_auth_complete(email="your@email.com", totp_code="654321")`.
|
|
558
|
+
|
|
552
559
|
### Diagnose Issues
|
|
553
560
|
|
|
554
561
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "perplexity-web-mcp-cli"
|
|
3
|
-
version = "0.14.
|
|
3
|
+
version = "0.14.4"
|
|
4
4
|
description = "CLI, MCP server, and Anthropic/OpenAI API-compatible interface for Perplexity AI."
|
|
5
5
|
authors = [{ name = "Jacob BD" }]
|
|
6
6
|
license = "MIT"
|
|
@@ -27,6 +27,7 @@ dependencies = [
|
|
|
27
27
|
"orjson>=3.11.6,<4.0",
|
|
28
28
|
"pydantic>=2.12.5,<3.0",
|
|
29
29
|
"tenacity>=9.1.2,<10.0",
|
|
30
|
+
"tomli>=2.0.0,<3.0; python_version < '3.11'",
|
|
30
31
|
"python-dotenv>=1.0.0,<2.0",
|
|
31
32
|
"rich>=13.0.0,<15.0",
|
|
32
33
|
"rich-click>=1.8",
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/api/server.py
RENAMED
|
@@ -1075,8 +1075,8 @@ async def stream_response(
|
|
|
1075
1075
|
|
|
1076
1076
|
elif kind == "error":
|
|
1077
1077
|
logging.error(f"Stream error: {payload}")
|
|
1078
|
-
#
|
|
1079
|
-
error_msg = payload
|
|
1078
|
+
# Strip wrapper prefixes for cleaner client-facing messages
|
|
1079
|
+
error_msg = payload.removeprefix("Failed to parse API response: ")
|
|
1080
1080
|
if "403" in payload or "forbidden" in payload.lower():
|
|
1081
1081
|
error_msg = (
|
|
1082
1082
|
"Session token expired (403). "
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Perplexity email and TOTP authentication protocol helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
|
|
8
|
+
from curl_cffi.requests import Session
|
|
9
|
+
|
|
10
|
+
from .constants import API_BASE_URL, API_VERSION, APP_HEADERS, SESSION_COOKIE_NAME
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
AUTH_CSRF_ENDPOINT = "/api/auth/csrf"
|
|
14
|
+
AUTH_OTP_REDIRECT_ENDPOINT = "/api/auth/otp-redirect-link"
|
|
15
|
+
AUTH_SIGNIN_ENDPOINT = "/api/auth/signin/email"
|
|
16
|
+
AUTH_TOTP_VERIFY_ENDPOINT = "/api/auth/totp-challenge/verify"
|
|
17
|
+
_REDIRECT_STATUSES = {301, 302, 303, 307, 308}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_auth_session() -> tuple[Session, str]:
|
|
21
|
+
"""Create a browser-like session and return it with a CSRF token."""
|
|
22
|
+
session = Session(
|
|
23
|
+
impersonate="chrome",
|
|
24
|
+
headers={**APP_HEADERS, "Referer": API_BASE_URL, "Origin": API_BASE_URL},
|
|
25
|
+
)
|
|
26
|
+
session.get(API_BASE_URL)
|
|
27
|
+
response = session.get(f"{API_BASE_URL}{AUTH_CSRF_ENDPOINT}")
|
|
28
|
+
|
|
29
|
+
if response.status_code != 200:
|
|
30
|
+
raise ValueError(f"Failed to obtain CSRF token: HTTP {response.status_code}.")
|
|
31
|
+
|
|
32
|
+
csrf = _response_json(response).get("csrfToken")
|
|
33
|
+
if not csrf:
|
|
34
|
+
raise ValueError("Failed to obtain CSRF token.")
|
|
35
|
+
|
|
36
|
+
return session, str(csrf)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def request_verification_code(session: Session, csrf: str, email: str) -> None:
|
|
40
|
+
"""Send an email verification code using an initialized auth session."""
|
|
41
|
+
response = session.post(
|
|
42
|
+
f"{API_BASE_URL}{AUTH_SIGNIN_ENDPOINT}?version={API_VERSION}&source=default",
|
|
43
|
+
json={
|
|
44
|
+
"email": email,
|
|
45
|
+
"csrfToken": csrf,
|
|
46
|
+
"useNumericOtp": "true",
|
|
47
|
+
"json": "true",
|
|
48
|
+
"callbackUrl": f"{API_BASE_URL}/?login-source=floatingSignup",
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if response.status_code != 200:
|
|
53
|
+
raise ValueError(f"Authentication request failed: {response.text}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve_redirect_url(session: Session, email: str, code_or_link: str) -> str:
|
|
57
|
+
"""Resolve an email OTP or magic link to the authentication callback URL."""
|
|
58
|
+
if code_or_link.startswith("http"):
|
|
59
|
+
return code_or_link
|
|
60
|
+
|
|
61
|
+
response = session.post(
|
|
62
|
+
f"{API_BASE_URL}{AUTH_OTP_REDIRECT_ENDPOINT}",
|
|
63
|
+
json={
|
|
64
|
+
"email": email,
|
|
65
|
+
"otp": code_or_link,
|
|
66
|
+
"redirectUrl": f"{API_BASE_URL}/?login-source=floatingSignup",
|
|
67
|
+
"emailLoginMethod": "web-otp",
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if response.status_code != 200:
|
|
72
|
+
raise ValueError("Invalid verification code.")
|
|
73
|
+
|
|
74
|
+
redirect_path = _response_json(response).get("redirect")
|
|
75
|
+
if not redirect_path:
|
|
76
|
+
raise ValueError("No redirect URL received.")
|
|
77
|
+
|
|
78
|
+
redirect = str(redirect_path)
|
|
79
|
+
return f"{API_BASE_URL}{redirect}" if redirect.startswith("/") else redirect
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def follow_auth_callback(session: Session, redirect_url: str) -> str | None:
|
|
83
|
+
"""Follow an auth callback, returning a challenge token when TOTP is required."""
|
|
84
|
+
response = session.get(redirect_url, allow_redirects=False)
|
|
85
|
+
if response.status_code not in _REDIRECT_STATUSES:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
location = response.headers.get("Location", "")
|
|
89
|
+
if not location:
|
|
90
|
+
raise ValueError("Authentication callback did not provide a redirect location.")
|
|
91
|
+
if "error=" in location:
|
|
92
|
+
raise ValueError("Verification failed. The code may be invalid or expired.")
|
|
93
|
+
|
|
94
|
+
if "/auth/totp-challenge" in location:
|
|
95
|
+
absolute_location = location if location.startswith("http") else f"{API_BASE_URL}{location}"
|
|
96
|
+
challenge_token = parse_qs(urlparse(absolute_location).query).get("token", [""])[0]
|
|
97
|
+
if not challenge_token:
|
|
98
|
+
raise ValueError("TOTP challenge token not found in redirect.")
|
|
99
|
+
return challenge_token
|
|
100
|
+
|
|
101
|
+
session.get(location if location.startswith("http") else f"{API_BASE_URL}{location}")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def verify_totp(session: Session, challenge_token: str, totp_code: str) -> None:
|
|
106
|
+
"""Complete a TOTP challenge and follow its post-verification redirect."""
|
|
107
|
+
if not totp_code.isdigit() or len(totp_code) != 6:
|
|
108
|
+
raise ValueError("TOTP code must be a 6-digit number.")
|
|
109
|
+
|
|
110
|
+
response = session.post(
|
|
111
|
+
f"{API_BASE_URL}{AUTH_TOTP_VERIFY_ENDPOINT}?version={API_VERSION}&source=default",
|
|
112
|
+
json={"token": challenge_token, "code": totp_code},
|
|
113
|
+
allow_redirects=False,
|
|
114
|
+
)
|
|
115
|
+
data = _response_json(response)
|
|
116
|
+
|
|
117
|
+
if response.status_code >= 400 or data.get("error"):
|
|
118
|
+
detail = data.get("error") or f"HTTP {response.status_code}"
|
|
119
|
+
raise ValueError(f"TOTP verification failed: {detail}")
|
|
120
|
+
|
|
121
|
+
location = response.headers.get("Location", "") if response.status_code in _REDIRECT_STATUSES else ""
|
|
122
|
+
redirect = location or str(data.get("redirect", ""))
|
|
123
|
+
if redirect:
|
|
124
|
+
session.get(redirect if redirect.startswith("http") else f"{API_BASE_URL}{redirect}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def extract_session_token(session: Session) -> str:
|
|
128
|
+
"""Return the session token, including a token split across cookie chunks."""
|
|
129
|
+
token = session.cookies.get(SESSION_COOKIE_NAME)
|
|
130
|
+
if token:
|
|
131
|
+
return token
|
|
132
|
+
|
|
133
|
+
chunks = sorted(
|
|
134
|
+
[
|
|
135
|
+
(name, value)
|
|
136
|
+
for name, value in session.cookies.items()
|
|
137
|
+
if name.startswith(f"{SESSION_COOKIE_NAME}.") and name.removeprefix(f"{SESSION_COOKIE_NAME}.").isdigit()
|
|
138
|
+
],
|
|
139
|
+
key=lambda item: int(item[0].removeprefix(f"{SESSION_COOKIE_NAME}.")),
|
|
140
|
+
)
|
|
141
|
+
if chunks:
|
|
142
|
+
return "".join(value for _, value in chunks)
|
|
143
|
+
|
|
144
|
+
raise ValueError("Authentication completed, but the session token cookie was not returned.")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _response_json(response: Any) -> dict[str, Any]:
|
|
148
|
+
"""Return a JSON object from a response, or an empty object for non-JSON bodies."""
|
|
149
|
+
try:
|
|
150
|
+
data = response.json()
|
|
151
|
+
except Exception:
|
|
152
|
+
return {}
|
|
153
|
+
return data if isinstance(data, dict) else {}
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/ai_doc.py
RENAMED
|
@@ -240,8 +240,8 @@ AUTH TOOLS (3):
|
|
|
240
240
|
pplx_auth_request_code(email)
|
|
241
241
|
Send 6-digit verification code to email.
|
|
242
242
|
|
|
243
|
-
pplx_auth_complete(email, code)
|
|
244
|
-
Complete
|
|
243
|
+
pplx_auth_complete(email, code, totp_code=None)
|
|
244
|
+
Complete email OTP and optional TOTP authentication. Saves token automatically.
|
|
245
245
|
|
|
246
246
|
================================================================================
|
|
247
247
|
AUTHENTICATION
|
|
@@ -255,10 +255,13 @@ Three ways to authenticate (all store token at ~/.config/perplexity-web-mcp/toke
|
|
|
255
255
|
2. NON-INTERACTIVE CLI (AI agent with shell access):
|
|
256
256
|
pwm login --email user@example.com # Sends code
|
|
257
257
|
pwm login --email user@example.com --code 123456 # Completes auth
|
|
258
|
+
pwm login --email user@example.com --code 123456 --totp-code 654321 # TOTP account
|
|
258
259
|
|
|
259
260
|
3. MCP TOOLS (AI agent without shell):
|
|
260
261
|
pplx_auth_request_code(email="user@example.com") # Sends code
|
|
261
|
-
pplx_auth_complete(email="user@example.com", code="123456") # Completes
|
|
262
|
+
pplx_auth_complete(email="user@example.com", code="123456") # Completes email OTP
|
|
263
|
+
# If TOTP_REQUIRED is returned:
|
|
264
|
+
pplx_auth_complete(email="user@example.com", totp_code="654321")
|
|
262
265
|
|
|
263
266
|
Tokens last ~30 days. Re-authenticate when you get 403 errors.
|
|
264
267
|
Check status: pwm login --check OR pplx_auth_status()
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/auth.py
RENAMED
|
@@ -8,18 +8,25 @@ from sys import exit
|
|
|
8
8
|
from typing import NoReturn
|
|
9
9
|
|
|
10
10
|
from curl_cffi.requests import Session
|
|
11
|
-
from orjson import loads
|
|
12
11
|
from rich.console import Console
|
|
13
12
|
from rich.panel import Panel
|
|
14
13
|
from rich.prompt import Prompt
|
|
15
14
|
from rich.table import Table
|
|
16
15
|
|
|
16
|
+
from perplexity_web_mcp.auth import (
|
|
17
|
+
create_auth_session,
|
|
18
|
+
extract_session_token,
|
|
19
|
+
follow_auth_callback,
|
|
20
|
+
request_verification_code,
|
|
21
|
+
resolve_redirect_url,
|
|
22
|
+
verify_totp,
|
|
23
|
+
)
|
|
24
|
+
from perplexity_web_mcp.constants import API_BASE_URL, APP_HEADERS, SESSION_COOKIE_NAME
|
|
17
25
|
from perplexity_web_mcp.token_store import load_token
|
|
18
26
|
from perplexity_web_mcp.token_store import save_token as save_token_to_config
|
|
19
27
|
|
|
20
28
|
|
|
21
|
-
BASE_URL: str =
|
|
22
|
-
SESSION_COOKIE_NAME: str = "__Secure-next-auth.session-token"
|
|
29
|
+
BASE_URL: str = API_BASE_URL
|
|
23
30
|
|
|
24
31
|
console = Console(stderr=True, soft_wrap=True)
|
|
25
32
|
|
|
@@ -96,7 +103,7 @@ def get_user_info(token: str) -> UserInfo | None:
|
|
|
96
103
|
try:
|
|
97
104
|
with Session(
|
|
98
105
|
impersonate="chrome",
|
|
99
|
-
headers={"Referer": BASE_URL, "Origin": BASE_URL},
|
|
106
|
+
headers={**APP_HEADERS, "Referer": BASE_URL, "Origin": BASE_URL},
|
|
100
107
|
cookies={SESSION_COOKIE_NAME: token},
|
|
101
108
|
) as session:
|
|
102
109
|
response = session.get(f"{BASE_URL}/api/user")
|
|
@@ -110,77 +117,45 @@ def get_user_info(token: str) -> UserInfo | None:
|
|
|
110
117
|
|
|
111
118
|
def _initialize_session() -> tuple[Session, str]:
|
|
112
119
|
"""Initialize session and obtain CSRF token."""
|
|
113
|
-
|
|
114
|
-
session = Session(impersonate="chrome", headers={"Referer": BASE_URL, "Origin": BASE_URL})
|
|
115
|
-
|
|
116
120
|
with console.status("[bold green]Initializing secure connection...", spinner="dots"):
|
|
117
|
-
|
|
118
|
-
csrf_data = loads(session.get(f"{BASE_URL}/api/auth/csrf").content)
|
|
119
|
-
csrf = csrf_data.get("csrfToken")
|
|
120
|
-
|
|
121
|
-
if not csrf:
|
|
122
|
-
raise ValueError("Failed to obtain CSRF token.")
|
|
123
|
-
|
|
124
|
-
return session, csrf
|
|
121
|
+
return create_auth_session()
|
|
125
122
|
|
|
126
123
|
|
|
127
124
|
def _request_verification_code(session: Session, csrf: str, email: str) -> None:
|
|
128
125
|
"""Send verification code to user's email."""
|
|
129
|
-
|
|
130
126
|
with console.status("[bold green]Sending verification code...", spinner="dots"):
|
|
131
|
-
|
|
132
|
-
f"{BASE_URL}/api/auth/signin/email?version=2.18&source=default",
|
|
133
|
-
json={
|
|
134
|
-
"email": email,
|
|
135
|
-
"csrfToken": csrf,
|
|
136
|
-
"useNumericOtp": "true",
|
|
137
|
-
"json": "true",
|
|
138
|
-
"callbackUrl": f"{BASE_URL}/?login-source=floatingSignup",
|
|
139
|
-
},
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
if response.status_code != 200:
|
|
143
|
-
raise ValueError(f"Authentication request failed: {response.text}")
|
|
127
|
+
request_verification_code(session, csrf, email)
|
|
144
128
|
|
|
145
129
|
|
|
146
130
|
def _validate_and_get_redirect_url(session: Session, email: str, user_input: str) -> str:
|
|
147
131
|
"""Validate user input (OTP or magic link) and return redirect URL."""
|
|
148
|
-
|
|
149
132
|
with console.status("[bold green]Validating...", spinner="dots"):
|
|
150
|
-
|
|
151
|
-
return user_input
|
|
152
|
-
|
|
153
|
-
response_otp = session.post(
|
|
154
|
-
f"{BASE_URL}/api/auth/otp-redirect-link",
|
|
155
|
-
json={
|
|
156
|
-
"email": email,
|
|
157
|
-
"otp": user_input,
|
|
158
|
-
"redirectUrl": f"{BASE_URL}/?login-source=floatingSignup",
|
|
159
|
-
"emailLoginMethod": "web-otp",
|
|
160
|
-
},
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
if response_otp.status_code != 200:
|
|
164
|
-
raise ValueError("Invalid verification code.")
|
|
165
|
-
|
|
166
|
-
redirect_path = loads(response_otp.content).get("redirect")
|
|
167
|
-
|
|
168
|
-
if not redirect_path:
|
|
169
|
-
raise ValueError("No redirect URL received.")
|
|
133
|
+
return resolve_redirect_url(session, email, user_input)
|
|
170
134
|
|
|
171
|
-
return f"{BASE_URL}{redirect_path}" if redirect_path.startswith("/") else redirect_path
|
|
172
135
|
|
|
136
|
+
def _complete_auth_callback(session: Session, redirect_url: str, totp_code: str | None = None) -> str:
|
|
137
|
+
"""Complete the callback and optional TOTP challenge, then return the session token."""
|
|
138
|
+
challenge_token = follow_auth_callback(session, redirect_url)
|
|
139
|
+
if challenge_token:
|
|
140
|
+
if not totp_code:
|
|
141
|
+
raise ValueError("TOTP is required. Re-run with --totp-code and the code from your authenticator app.")
|
|
142
|
+
verify_totp(session, challenge_token, totp_code)
|
|
143
|
+
return extract_session_token(session)
|
|
173
144
|
|
|
174
|
-
def _extract_session_token(session: Session, redirect_url: str) -> str:
|
|
175
|
-
"""Extract session token from cookies after authentication."""
|
|
176
145
|
|
|
177
|
-
|
|
178
|
-
|
|
146
|
+
def _prompt_and_verify_totp(session: Session, challenge_token: str) -> None:
|
|
147
|
+
"""Prompt until a valid TOTP code completes the challenge."""
|
|
148
|
+
console.print("\n[bold cyan]Step 3: Two-Factor Authentication[/bold cyan]")
|
|
149
|
+
console.print(" Enter the 6-digit code from your authenticator app.")
|
|
179
150
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
151
|
+
while True:
|
|
152
|
+
totp_code = Prompt.ask(" Enter TOTP code", console=console).strip()
|
|
153
|
+
try:
|
|
154
|
+
with console.status("[bold green]Verifying TOTP...", spinner="dots"):
|
|
155
|
+
verify_totp(session, challenge_token, totp_code)
|
|
156
|
+
return
|
|
157
|
+
except ValueError as error:
|
|
158
|
+
console.print(f"[red] {error}[/red]")
|
|
184
159
|
|
|
185
160
|
|
|
186
161
|
def _display_user_info(user_info: UserInfo) -> None:
|
|
@@ -245,13 +220,19 @@ def _show_exit_message() -> None:
|
|
|
245
220
|
console.input()
|
|
246
221
|
|
|
247
222
|
|
|
248
|
-
def auth_non_interactive(
|
|
223
|
+
def auth_non_interactive(
|
|
224
|
+
email: str,
|
|
225
|
+
code: str | None = None,
|
|
226
|
+
auto_save: bool = True,
|
|
227
|
+
totp_code: str | None = None,
|
|
228
|
+
) -> str | None:
|
|
249
229
|
"""Non-interactive authentication for AI agents.
|
|
250
230
|
|
|
251
231
|
Args:
|
|
252
232
|
email: Perplexity account email
|
|
253
233
|
code: 6-digit verification code (if None, sends code and returns None)
|
|
254
234
|
auto_save: Whether to automatically save token to config
|
|
235
|
+
totp_code: Optional 6-digit authenticator code for accounts with TOTP enabled
|
|
255
236
|
|
|
256
237
|
Returns:
|
|
257
238
|
Session token if code provided, None if code was sent
|
|
@@ -275,7 +256,7 @@ def auth_non_interactive(email: str, code: str | None = None, auto_save: bool =
|
|
|
275
256
|
|
|
276
257
|
# Step 2: Complete authentication
|
|
277
258
|
redirect_url = _validate_and_get_redirect_url(session, email, code)
|
|
278
|
-
token =
|
|
259
|
+
token = _complete_auth_callback(session, redirect_url, totp_code)
|
|
279
260
|
|
|
280
261
|
# Verify token works
|
|
281
262
|
user_info = get_user_info(token)
|
|
@@ -318,6 +299,7 @@ def main() -> NoReturn:
|
|
|
318
299
|
" --check Check if authenticated without logging in\n"
|
|
319
300
|
" --email EMAIL Email address for non-interactive auth\n"
|
|
320
301
|
" --code CODE 6-digit verification code from email\n"
|
|
302
|
+
" --totp-code CODE 6-digit code from your authenticator app\n"
|
|
321
303
|
" --no-save Don't save token to config (non-interactive only)\n"
|
|
322
304
|
" -h, --help Show this help message\n\n"
|
|
323
305
|
"[bold cyan]Token Storage:[/bold cyan]\n"
|
|
@@ -364,13 +346,18 @@ def main() -> NoReturn:
|
|
|
364
346
|
code_idx = args.index("--code")
|
|
365
347
|
code = args[code_idx + 1] if code_idx + 1 < len(args) else None
|
|
366
348
|
|
|
349
|
+
totp_code = None
|
|
350
|
+
if "--totp-code" in args:
|
|
351
|
+
totp_idx = args.index("--totp-code")
|
|
352
|
+
totp_code = args[totp_idx + 1] if totp_idx + 1 < len(args) else None
|
|
353
|
+
|
|
367
354
|
no_save = "--no-save" in args
|
|
368
355
|
|
|
369
356
|
if not email:
|
|
370
357
|
print("Error: --email requires an email address")
|
|
371
358
|
exit(1)
|
|
372
359
|
|
|
373
|
-
result = auth_non_interactive(email, code, auto_save=not no_save)
|
|
360
|
+
result = auth_non_interactive(email, code, auto_save=not no_save, totp_code=totp_code)
|
|
374
361
|
exit(0 if result or code is None else 1)
|
|
375
362
|
|
|
376
363
|
# Interactive mode (original behavior)
|
|
@@ -387,8 +374,10 @@ def main() -> NoReturn:
|
|
|
387
374
|
console.print(" Check your email for a [bold]6-digit code[/bold] or [bold]magic link[/bold].")
|
|
388
375
|
user_input = Prompt.ask(" Enter code or paste link", console=console).strip()
|
|
389
376
|
redirect_url = _validate_and_get_redirect_url(session, email, user_input)
|
|
390
|
-
|
|
391
|
-
|
|
377
|
+
challenge_token = follow_auth_callback(session, redirect_url)
|
|
378
|
+
if challenge_token:
|
|
379
|
+
_prompt_and_verify_totp(session, challenge_token)
|
|
380
|
+
token = extract_session_token(session)
|
|
392
381
|
|
|
393
382
|
_display_and_save_token(token)
|
|
394
383
|
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/setup.py
RENAMED
|
@@ -22,7 +22,12 @@ from rich.prompt import Confirm, Prompt
|
|
|
22
22
|
from rich.syntax import Syntax
|
|
23
23
|
from rich.table import Table
|
|
24
24
|
import rich_click as click
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import tomllib
|
|
29
|
+
except ModuleNotFoundError: # Python 3.10
|
|
30
|
+
import tomli as tomllib
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
console = Console()
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/constants.py
RENAMED
|
@@ -9,6 +9,12 @@ from typing import Final
|
|
|
9
9
|
API_VERSION: Final[str] = "2.18"
|
|
10
10
|
"""Current API version used by Perplexity WebUI."""
|
|
11
11
|
|
|
12
|
+
APP_HEADERS: Final[dict[str, str]] = {
|
|
13
|
+
"x-app-apiclient": "default",
|
|
14
|
+
"x-app-apiversion": API_VERSION,
|
|
15
|
+
}
|
|
16
|
+
"""Headers used by the Perplexity web application for internal API requests."""
|
|
17
|
+
|
|
12
18
|
API_BASE_URL: Final[str] = "https://www.perplexity.ai"
|
|
13
19
|
"""Base URL for all API requests."""
|
|
14
20
|
|
|
@@ -52,6 +58,7 @@ JSON_OBJECT_PATTERN: Final[Pattern[str]] = compile(r"^\{.*\}$")
|
|
|
52
58
|
"""Pattern to detect JSON object strings."""
|
|
53
59
|
|
|
54
60
|
DEFAULT_HEADERS: Final[dict[str, str]] = {
|
|
61
|
+
**APP_HEADERS,
|
|
55
62
|
"Accept": "text/event-stream, application/json",
|
|
56
63
|
"Content-Type": "application/json",
|
|
57
64
|
}
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/core.py
RENAMED
|
@@ -590,10 +590,12 @@ class Conversation:
|
|
|
590
590
|
except KeyError as error:
|
|
591
591
|
raise ResponseParsingError("Missing 'text' field in data", raw_data=str(data)) from error
|
|
592
592
|
except JSONDecodeError as error:
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
593
|
+
raw_text = data.get("text", "")
|
|
594
|
+
logger.warning(f"Non-JSON text in SSE data: {raw_text[:200]}")
|
|
595
|
+
stripped = raw_text.lstrip()
|
|
596
|
+
if stripped and not stripped.startswith(("{", "[")):
|
|
597
|
+
raise ResponseParsingError(raw_text) from error
|
|
598
|
+
raise ResponseParsingError("Invalid JSON in 'text' field", raw_data=str(raw_text)[:500]) from error
|
|
597
599
|
|
|
598
600
|
answer_data: dict[str, Any] = {}
|
|
599
601
|
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/data/SKILL.md
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: perplexity-web-mcp
|
|
3
3
|
description: 'Search the web and query AI models via Perplexity AI using perplexity-web-mcp-cli. Supports CLI commands (pwm ask, pwm research), MCP tools (pplx_*), and Anthropic/OpenAI-compatible API server. Use when the user mentions "perplexity", "pplx", "pwm", "web search with AI", "deep research", "search the internet", or wants to query premium models like GPT-5.6 Terra, GPT-5.6 Sol, Grok, Claude, Gemini, GLM, or Nemotron through Perplexity''s web interface.'
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.14.
|
|
5
|
+
version: "0.14.4"
|
|
6
6
|
author: "Jacob BD"
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -209,8 +209,8 @@ User wants to...
|
|
|
209
209
|
|
|
|
210
210
|
+-- Authenticate / re-authenticate
|
|
211
211
|
| +-- Interactive: pwm login
|
|
212
|
-
| +-- Non-interactive: pwm login --email EMAIL
|
|
213
|
-
| +-- MCP (no shell): pplx_auth_request_code(email)
|
|
212
|
+
| +-- Non-interactive: pwm login --email EMAIL, then pwm login --email EMAIL --code CODE [--totp-code CODE]
|
|
213
|
+
| +-- MCP (no shell): pplx_auth_request_code(email), then pplx_auth_complete(email, code[, totp_code])
|
|
214
214
|
|
|
|
215
215
|
+-- Start MCP server
|
|
216
216
|
| +-- pwm-mcp
|
|
@@ -326,8 +326,11 @@ pwm login # Interactive
|
|
|
326
326
|
pwm login --check # Check status
|
|
327
327
|
pwm login --email user@example.com # Send code
|
|
328
328
|
pwm login --email user@example.com --code 123456 # Complete
|
|
329
|
+
pwm login --email user@example.com --code 123456 --totp-code 654321 # Complete with TOTP
|
|
329
330
|
```
|
|
330
331
|
|
|
332
|
+
Set `PWM_SAVE_TO_LIBRARY=1` to save shared CLI and MCP queries to the Perplexity thread library. Queries are incognito by default.
|
|
333
|
+
|
|
331
334
|
### Usage
|
|
332
335
|
|
|
333
336
|
```bash
|
|
@@ -360,7 +363,7 @@ pwm usage --refresh # Force-refresh from server
|
|
|
360
363
|
| `pplx_connectors` | FREE | List account connector source IDs for `source_focus` |
|
|
361
364
|
| `pplx_auth_status` | FREE | Check auth status |
|
|
362
365
|
| `pplx_auth_request_code` | FREE | Send verification code |
|
|
363
|
-
| `pplx_auth_complete` | FREE | Complete
|
|
366
|
+
| `pplx_auth_complete` | FREE | Complete email and optional TOTP authentication |
|
|
364
367
|
|
|
365
368
|
All query tools accept `source_focus`: `"none"`, `"web"`, `"academic"`, `"social"`, `"finance"`, `"all"`, or a connector source ID from `pplx_connectors()`.
|
|
366
369
|
Use `source_focus="none"` for model-only queries without web search.
|
|
@@ -513,6 +516,7 @@ pwm login --check && pwm usage
|
|
|
513
516
|
pwm login --email user@example.com
|
|
514
517
|
# wait for email, then:
|
|
515
518
|
pwm login --email user@example.com --code 123456
|
|
519
|
+
# For a TOTP-enabled account, include: --totp-code 654321
|
|
516
520
|
```
|
|
517
521
|
|
|
518
522
|
## API Server
|
|
@@ -255,10 +255,13 @@ Complete authentication with the verification code. Second step of re-authentica
|
|
|
255
255
|
```
|
|
256
256
|
pplx_auth_complete(
|
|
257
257
|
email: str, # Required. Same email used in pplx_auth_request_code.
|
|
258
|
-
code: str,
|
|
258
|
+
code: str = "", # 6-digit code from email.
|
|
259
|
+
totp_code: str | None = None, # Authenticator code when TOTP is enabled.
|
|
259
260
|
) -> str
|
|
260
261
|
```
|
|
261
262
|
|
|
263
|
+
If the first completion call returns `TOTP_REQUIRED`, call the tool again with `email` and `totp_code`.
|
|
264
|
+
|
|
262
265
|
## Response Format
|
|
263
266
|
|
|
264
267
|
All query tools return a string containing:
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/mcp/server.py
RENAMED
|
@@ -13,6 +13,14 @@ import uuid
|
|
|
13
13
|
|
|
14
14
|
from fastmcp import FastMCP
|
|
15
15
|
|
|
16
|
+
from perplexity_web_mcp.auth import (
|
|
17
|
+
create_auth_session,
|
|
18
|
+
extract_session_token,
|
|
19
|
+
follow_auth_callback,
|
|
20
|
+
request_verification_code,
|
|
21
|
+
resolve_redirect_url,
|
|
22
|
+
verify_totp,
|
|
23
|
+
)
|
|
16
24
|
from perplexity_web_mcp.models import Models
|
|
17
25
|
from perplexity_web_mcp.shared import (
|
|
18
26
|
COUNCIL_DEFAULT_MODELS_STR,
|
|
@@ -74,7 +82,7 @@ mcp = FastMCP(
|
|
|
74
82
|
"AUTHENTICATION: If you get a 403 error or 'token expired' message:\n"
|
|
75
83
|
"1. pplx_auth_status — check current authentication status\n"
|
|
76
84
|
"2. pplx_auth_request_code — send verification code to email\n"
|
|
77
|
-
"3. pplx_auth_complete — complete
|
|
85
|
+
"3. pplx_auth_complete — complete email OTP and any requested TOTP challenge"
|
|
78
86
|
),
|
|
79
87
|
)
|
|
80
88
|
|
|
@@ -683,33 +691,9 @@ def pplx_auth_request_code(email: str) -> str:
|
|
|
683
691
|
Returns:
|
|
684
692
|
Status message indicating if the code was sent successfully
|
|
685
693
|
"""
|
|
686
|
-
from curl_cffi.requests import Session
|
|
687
|
-
from orjson import loads
|
|
688
|
-
|
|
689
|
-
BASE_URL = "https://www.perplexity.ai"
|
|
690
|
-
|
|
691
694
|
try:
|
|
692
|
-
session =
|
|
693
|
-
session
|
|
694
|
-
csrf_data = loads(session.get(f"{BASE_URL}/api/auth/csrf").content)
|
|
695
|
-
csrf = csrf_data.get("csrfToken")
|
|
696
|
-
|
|
697
|
-
if not csrf:
|
|
698
|
-
return "ERROR: Failed to obtain CSRF token. Please try again."
|
|
699
|
-
|
|
700
|
-
response = session.post(
|
|
701
|
-
f"{BASE_URL}/api/auth/signin/email?version=2.18&source=default",
|
|
702
|
-
json={
|
|
703
|
-
"email": email,
|
|
704
|
-
"csrfToken": csrf,
|
|
705
|
-
"useNumericOtp": "true",
|
|
706
|
-
"json": "true",
|
|
707
|
-
"callbackUrl": f"{BASE_URL}/?login-source=floatingSignup",
|
|
708
|
-
},
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
if response.status_code != 200:
|
|
712
|
-
return f"ERROR: Failed to send verification code. Status: {response.status_code}"
|
|
695
|
+
session, csrf = create_auth_session()
|
|
696
|
+
request_verification_code(session, csrf, email)
|
|
713
697
|
|
|
714
698
|
_set_auth_session({"session": session, "email": email})
|
|
715
699
|
|
|
@@ -725,7 +709,7 @@ def pplx_auth_request_code(email: str) -> str:
|
|
|
725
709
|
|
|
726
710
|
|
|
727
711
|
@mcp.tool
|
|
728
|
-
def pplx_auth_complete(email: str, code: str) -> str:
|
|
712
|
+
def pplx_auth_complete(email: str, code: str = "", totp_code: str | None = None) -> str:
|
|
729
713
|
"""Complete Perplexity authentication with the verification code.
|
|
730
714
|
|
|
731
715
|
Use the 6-digit code received via email after calling pplx_auth_request_code.
|
|
@@ -734,50 +718,37 @@ def pplx_auth_complete(email: str, code: str) -> str:
|
|
|
734
718
|
Args:
|
|
735
719
|
email: Your Perplexity account email (same as used in pplx_auth_request_code)
|
|
736
720
|
code: The 6-digit verification code from your email
|
|
721
|
+
totp_code: The 6-digit authenticator code when TOTP is enabled
|
|
737
722
|
|
|
738
723
|
Returns:
|
|
739
724
|
Status message with authentication result and subscription tier
|
|
740
725
|
"""
|
|
741
|
-
from curl_cffi.requests import Session
|
|
742
|
-
from orjson import loads
|
|
743
|
-
|
|
744
726
|
from perplexity_web_mcp.cli.auth import get_user_info
|
|
745
727
|
|
|
746
|
-
BASE_URL = "https://www.perplexity.ai"
|
|
747
|
-
SESSION_COOKIE_NAME = "__Secure-next-auth.session-token"
|
|
748
|
-
|
|
749
728
|
try:
|
|
750
729
|
stored = _get_auth_session(email)
|
|
751
730
|
if stored:
|
|
752
731
|
session = stored["session"]
|
|
753
732
|
else:
|
|
754
|
-
session =
|
|
755
|
-
session.get(BASE_URL)
|
|
756
|
-
|
|
757
|
-
response = session.post(
|
|
758
|
-
f"{BASE_URL}/api/auth/otp-redirect-link",
|
|
759
|
-
json={
|
|
760
|
-
"email": email,
|
|
761
|
-
"otp": code,
|
|
762
|
-
"redirectUrl": f"{BASE_URL}/?login-source=floatingSignup",
|
|
763
|
-
"emailLoginMethod": "web-otp",
|
|
764
|
-
},
|
|
765
|
-
)
|
|
766
|
-
|
|
767
|
-
if response.status_code != 200:
|
|
768
|
-
return "ERROR: Invalid verification code. Please check and try again."
|
|
769
|
-
|
|
770
|
-
redirect_path = loads(response.content).get("redirect")
|
|
771
|
-
if not redirect_path:
|
|
772
|
-
return "ERROR: No redirect URL received. Please try again."
|
|
733
|
+
session, _ = create_auth_session()
|
|
773
734
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
735
|
+
challenge_token = stored.get("challenge_token") if stored else None
|
|
736
|
+
if challenge_token:
|
|
737
|
+
if not totp_code:
|
|
738
|
+
return "TOTP_REQUIRED: Call pplx_auth_complete again with email and totp_code."
|
|
739
|
+
verify_totp(session, challenge_token, totp_code)
|
|
740
|
+
else:
|
|
741
|
+
if not code:
|
|
742
|
+
return "ERROR: The 6-digit email verification code is required."
|
|
743
|
+
redirect_url = resolve_redirect_url(session, email, code)
|
|
744
|
+
challenge_token = follow_auth_callback(session, redirect_url)
|
|
745
|
+
if challenge_token:
|
|
746
|
+
_set_auth_session({"session": session, "email": email, "challenge_token": challenge_token})
|
|
747
|
+
if not totp_code:
|
|
748
|
+
return "TOTP_REQUIRED: Call pplx_auth_complete again with email and totp_code."
|
|
749
|
+
verify_totp(session, challenge_token, totp_code)
|
|
750
|
+
|
|
751
|
+
token = extract_session_token(session)
|
|
781
752
|
|
|
782
753
|
if save_token(token):
|
|
783
754
|
_clear_auth_session()
|
|
@@ -17,7 +17,14 @@ from typing import Any
|
|
|
17
17
|
|
|
18
18
|
from curl_cffi.requests import Session
|
|
19
19
|
|
|
20
|
-
from .constants import
|
|
20
|
+
from .constants import (
|
|
21
|
+
API_BASE_URL,
|
|
22
|
+
APP_HEADERS,
|
|
23
|
+
ENDPOINT_CREDITS,
|
|
24
|
+
ENDPOINT_RATE_LIMITS,
|
|
25
|
+
ENDPOINT_USER_SETTINGS,
|
|
26
|
+
SESSION_COOKIE_NAME,
|
|
27
|
+
)
|
|
21
28
|
from .logging import get_logger
|
|
22
29
|
|
|
23
30
|
|
|
@@ -304,7 +311,12 @@ def _create_session(token: str) -> Session:
|
|
|
304
311
|
"""Create a minimal session for REST API calls."""
|
|
305
312
|
return Session(
|
|
306
313
|
impersonate="chrome",
|
|
307
|
-
headers={
|
|
314
|
+
headers={
|
|
315
|
+
**APP_HEADERS,
|
|
316
|
+
"Referer": API_BASE_URL,
|
|
317
|
+
"Origin": API_BASE_URL,
|
|
318
|
+
"Accept": "application/json",
|
|
319
|
+
},
|
|
308
320
|
cookies={SESSION_COOKIE_NAME: token},
|
|
309
321
|
)
|
|
310
322
|
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/shared.py
RENAMED
|
@@ -278,6 +278,11 @@ def _shared_client_config_from_env() -> ClientConfig:
|
|
|
278
278
|
)
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
def _save_to_library_from_env() -> bool:
|
|
282
|
+
"""Return whether shared CLI/MCP queries should be saved to the library."""
|
|
283
|
+
return environ.get("PWM_SAVE_TO_LIBRARY", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
284
|
+
|
|
285
|
+
|
|
281
286
|
def get_client() -> Perplexity:
|
|
282
287
|
"""Get or create a cached Perplexity client.
|
|
283
288
|
|
|
@@ -390,6 +395,7 @@ def _execute_query(
|
|
|
390
395
|
citation_mode=CitationMode.DEFAULT,
|
|
391
396
|
search_focus=search_focus,
|
|
392
397
|
source_focus=sources,
|
|
398
|
+
save_to_library=_save_to_library_from_env(),
|
|
393
399
|
)
|
|
394
400
|
)
|
|
395
401
|
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/doctor.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/hack.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/main.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/cli/skill.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/config.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/council.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/enums.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/exceptions.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/http.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/limits.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/models.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/py.typed
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/resilience.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/router.py
RENAMED
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/sessions.py
RENAMED
|
File without changes
|
|
File without changes
|
{perplexity_web_mcp_cli-0.14.2 → perplexity_web_mcp_cli-0.14.4}/src/perplexity_web_mcp/types.py
RENAMED
|
File without changes
|