plurity-mcp 0.1.0__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.
@@ -0,0 +1,24 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write # required for OIDC trusted publishing
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: astral-sh/setup-uv@v5
19
+
20
+ - name: Build
21
+ run: uv build
22
+
23
+ - name: Publish
24
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Plurity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: plurity-mcp
3
+ Version: 0.1.0
4
+ Summary: Unified MCP server for all Plurity services (Audit, Toll, Intelligence)
5
+ Project-URL: Homepage, https://plurity.ai
6
+ Project-URL: Repository, https://github.com/plurity-ai/plurity-mcp
7
+ Project-URL: Bug Tracker, https://github.com/plurity-ai/plurity-mcp/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: httpx>=0.27.0
16
+ Requires-Dist: mcp>=1.6.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # plurity-mcp
20
+
21
+ Unified [MCP](https://modelcontextprotocol.io) server for all Plurity services — **Audit**, **Toll**, and **Intelligence** — using a single API key.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ uvx plurity-mcp
27
+ ```
28
+
29
+ Or install permanently:
30
+
31
+ ```bash
32
+ uv tool install plurity-mcp
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ **1. Get an API key**
38
+
39
+ Log in to [account.plurity.ai](https://account.plurity.ai) → Settings → API Keys → Create key.
40
+ Grant the scopes for each service you want to use (`audit`, `toll`, `intelligence`).
41
+
42
+ **2. Run the setup wizard**
43
+
44
+ ```bash
45
+ plurity-mcp-setup
46
+ ```
47
+
48
+ The wizard validates your key and saves it to `~/.config/plurity/config.toml`.
49
+
50
+ **3. Add to your MCP client**
51
+
52
+ Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "plurity": {
58
+ "command": "uvx",
59
+ "args": ["plurity-mcp"],
60
+ "env": {
61
+ "PLURITY_API_KEY": "plt_your_key_here"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ ### Environment variables
71
+
72
+ | Variable | Default | Description |
73
+ |---|---|---|
74
+ | `PLURITY_API_KEY` | — | Your `plt_*` API key (**required**) |
75
+ | `PLURITY_ACCOUNTS_URL` | `https://account.plurity.ai` | Override accounts API URL |
76
+ | `PLURITY_AUDIT_URL` | `https://audit.plurity.ai` | Override Audit API URL |
77
+ | `PLURITY_TOLL_URL` | `https://toll.plurity.ai` | Override Toll API URL |
78
+ | `PLURITY_INTELLIGENCE_URL` | `https://intelligence.plurity.ai` | Override Intelligence API URL |
79
+ | `PLURITY_AUDIT_ENABLED` | `true` | Set to `false` to disable Audit tools |
80
+ | `PLURITY_TOLL_ENABLED` | `true` | Set to `false` to disable Toll tools |
81
+ | `PLURITY_INTELLIGENCE_ENABLED` | `true` | Set to `false` to disable Intelligence tools |
82
+
83
+ ### Config file (`~/.config/plurity/config.toml`)
84
+
85
+ ```toml
86
+ [mcp]
87
+ api_key = "plt_..."
88
+ accounts_url = "https://account.plurity.ai"
89
+ audit_base_url = "https://audit.plurity.ai"
90
+ toll_base_url = "https://toll.plurity.ai"
91
+ intelligence_base_url = "https://intelligence.plurity.ai"
92
+ audit_enabled = true
93
+ toll_enabled = true
94
+ intelligence_enabled = true
95
+ ```
96
+
97
+ Environment variables take precedence over the config file.
98
+
99
+ ### Service activation
100
+
101
+ On startup the server:
102
+
103
+ 1. Validates your API key against plurity-accounts
104
+ 2. Reads the key's scopes (e.g. `["audit", "toll"]`)
105
+ 3. Registers only the tools for services the key is scoped for
106
+ 4. Applies your `*_ENABLED` overrides — you can disable services the key has access to, but you cannot enable services the key lacks a scope for
107
+
108
+ If the key is invalid the server exits immediately with a clear error message visible in your MCP client.
109
+
110
+ ## Tools
111
+
112
+ ### Audit (GEO readiness)
113
+
114
+ | Tool | Description |
115
+ |---|---|
116
+ | `submit_audit_scan` | Queue a URL for AI-readiness analysis |
117
+ | `get_audit_scan` | Get scan status and results by ID |
118
+ | `get_audit_scan_by_url` | Look up the latest scan for a URL |
119
+ | `run_audit` | Submit and wait until complete (blocking) |
120
+
121
+ ### Toll (agent traffic + llms.txt)
122
+
123
+ | Tool | Description |
124
+ |---|---|
125
+ | `list_toll_sites` | List your sites/pages |
126
+ | `create_toll_site` | Create a new site |
127
+ | `get_toll_site` | Get site details including site key |
128
+ | `update_toll_site` | Update name, domain, cache TTL, llms.txt mode |
129
+ | `get_toll_installation_instructions` | Get integration code snippets for HTML/Next.js/React/GTM |
130
+ | `list_toll_qa_pairs` | List Q&A pairs in a site's llms.txt |
131
+ | `create_toll_qa_pair` | Add a Q&A entry to llms.txt |
132
+ | `update_toll_qa_pair` | Edit a Q&A entry (or publish/unpublish it) |
133
+ | `delete_toll_qa_pair` | Remove a Q&A entry |
134
+ | `get_toll_traffic` | Agent traffic chart data (today/week/month/year) |
135
+
136
+ ### Intelligence (question & topic monitoring)
137
+
138
+ | Tool | Description |
139
+ |---|---|
140
+ | `list_intelligence_sources` | Browse the global source catalogue |
141
+ | `list_subscribed_intelligence_sources` | List sources your org follows |
142
+ | `subscribe_intelligence_source` | Subscribe to a source |
143
+ | `unsubscribe_intelligence_source` | Unsubscribe from a source |
144
+ | `request_intelligence_source` | Add a new source by URL (auto-subscribes) |
145
+ | `list_intelligence_source_content` | List raw scraped documents |
146
+ | `list_intelligence_qa_pairs` | Browse the Q&A feed with filters |
147
+ | `approve_intelligence_qa_pair` | Move a Q&A pair into your knowledge base |
148
+ | `skip_intelligence_qa_pair` | Dismiss a Q&A pair from the feed |
149
+
150
+ ## Development
151
+
152
+ ```bash
153
+ git clone https://github.com/plurity-oss/plurity-mcp
154
+ cd plurity-mcp
155
+ uv sync
156
+ uv run plurity-mcp-setup # set up key
157
+ uv run plurity-mcp # run locally
158
+ ```
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,144 @@
1
+ # plurity-mcp
2
+
3
+ Unified [MCP](https://modelcontextprotocol.io) server for all Plurity services — **Audit**, **Toll**, and **Intelligence** — using a single API key.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uvx plurity-mcp
9
+ ```
10
+
11
+ Or install permanently:
12
+
13
+ ```bash
14
+ uv tool install plurity-mcp
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ **1. Get an API key**
20
+
21
+ Log in to [account.plurity.ai](https://account.plurity.ai) → Settings → API Keys → Create key.
22
+ Grant the scopes for each service you want to use (`audit`, `toll`, `intelligence`).
23
+
24
+ **2. Run the setup wizard**
25
+
26
+ ```bash
27
+ plurity-mcp-setup
28
+ ```
29
+
30
+ The wizard validates your key and saves it to `~/.config/plurity/config.toml`.
31
+
32
+ **3. Add to your MCP client**
33
+
34
+ Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "plurity": {
40
+ "command": "uvx",
41
+ "args": ["plurity-mcp"],
42
+ "env": {
43
+ "PLURITY_API_KEY": "plt_your_key_here"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ ### Environment variables
53
+
54
+ | Variable | Default | Description |
55
+ |---|---|---|
56
+ | `PLURITY_API_KEY` | — | Your `plt_*` API key (**required**) |
57
+ | `PLURITY_ACCOUNTS_URL` | `https://account.plurity.ai` | Override accounts API URL |
58
+ | `PLURITY_AUDIT_URL` | `https://audit.plurity.ai` | Override Audit API URL |
59
+ | `PLURITY_TOLL_URL` | `https://toll.plurity.ai` | Override Toll API URL |
60
+ | `PLURITY_INTELLIGENCE_URL` | `https://intelligence.plurity.ai` | Override Intelligence API URL |
61
+ | `PLURITY_AUDIT_ENABLED` | `true` | Set to `false` to disable Audit tools |
62
+ | `PLURITY_TOLL_ENABLED` | `true` | Set to `false` to disable Toll tools |
63
+ | `PLURITY_INTELLIGENCE_ENABLED` | `true` | Set to `false` to disable Intelligence tools |
64
+
65
+ ### Config file (`~/.config/plurity/config.toml`)
66
+
67
+ ```toml
68
+ [mcp]
69
+ api_key = "plt_..."
70
+ accounts_url = "https://account.plurity.ai"
71
+ audit_base_url = "https://audit.plurity.ai"
72
+ toll_base_url = "https://toll.plurity.ai"
73
+ intelligence_base_url = "https://intelligence.plurity.ai"
74
+ audit_enabled = true
75
+ toll_enabled = true
76
+ intelligence_enabled = true
77
+ ```
78
+
79
+ Environment variables take precedence over the config file.
80
+
81
+ ### Service activation
82
+
83
+ On startup the server:
84
+
85
+ 1. Validates your API key against plurity-accounts
86
+ 2. Reads the key's scopes (e.g. `["audit", "toll"]`)
87
+ 3. Registers only the tools for services the key is scoped for
88
+ 4. Applies your `*_ENABLED` overrides — you can disable services the key has access to, but you cannot enable services the key lacks a scope for
89
+
90
+ If the key is invalid the server exits immediately with a clear error message visible in your MCP client.
91
+
92
+ ## Tools
93
+
94
+ ### Audit (GEO readiness)
95
+
96
+ | Tool | Description |
97
+ |---|---|
98
+ | `submit_audit_scan` | Queue a URL for AI-readiness analysis |
99
+ | `get_audit_scan` | Get scan status and results by ID |
100
+ | `get_audit_scan_by_url` | Look up the latest scan for a URL |
101
+ | `run_audit` | Submit and wait until complete (blocking) |
102
+
103
+ ### Toll (agent traffic + llms.txt)
104
+
105
+ | Tool | Description |
106
+ |---|---|
107
+ | `list_toll_sites` | List your sites/pages |
108
+ | `create_toll_site` | Create a new site |
109
+ | `get_toll_site` | Get site details including site key |
110
+ | `update_toll_site` | Update name, domain, cache TTL, llms.txt mode |
111
+ | `get_toll_installation_instructions` | Get integration code snippets for HTML/Next.js/React/GTM |
112
+ | `list_toll_qa_pairs` | List Q&A pairs in a site's llms.txt |
113
+ | `create_toll_qa_pair` | Add a Q&A entry to llms.txt |
114
+ | `update_toll_qa_pair` | Edit a Q&A entry (or publish/unpublish it) |
115
+ | `delete_toll_qa_pair` | Remove a Q&A entry |
116
+ | `get_toll_traffic` | Agent traffic chart data (today/week/month/year) |
117
+
118
+ ### Intelligence (question & topic monitoring)
119
+
120
+ | Tool | Description |
121
+ |---|---|
122
+ | `list_intelligence_sources` | Browse the global source catalogue |
123
+ | `list_subscribed_intelligence_sources` | List sources your org follows |
124
+ | `subscribe_intelligence_source` | Subscribe to a source |
125
+ | `unsubscribe_intelligence_source` | Unsubscribe from a source |
126
+ | `request_intelligence_source` | Add a new source by URL (auto-subscribes) |
127
+ | `list_intelligence_source_content` | List raw scraped documents |
128
+ | `list_intelligence_qa_pairs` | Browse the Q&A feed with filters |
129
+ | `approve_intelligence_qa_pair` | Move a Q&A pair into your knowledge base |
130
+ | `skip_intelligence_qa_pair` | Dismiss a Q&A pair from the feed |
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ git clone https://github.com/plurity-oss/plurity-mcp
136
+ cd plurity-mcp
137
+ uv sync
138
+ uv run plurity-mcp-setup # set up key
139
+ uv run plurity-mcp # run locally
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "plurity-mcp"
3
+ version = "0.1.0"
4
+ description = "Unified MCP server for all Plurity services (Audit, Toll, Intelligence)"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "mcp>=1.6.0",
10
+ "httpx>=0.27.0",
11
+ ]
12
+
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://plurity.ai"
22
+ Repository = "https://github.com/plurity-ai/plurity-mcp"
23
+ "Bug Tracker" = "https://github.com/plurity-ai/plurity-mcp/issues"
24
+
25
+ [project.scripts]
26
+ plurity-mcp = "plurity_mcp.server:main"
27
+ plurity-mcp-setup = "plurity_mcp.setup:main"
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/plurity_mcp"]
@@ -0,0 +1,3 @@
1
+ """Plurity unified MCP server."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,286 @@
1
+ """Synchronous HTTP clients for each Plurity service.
2
+
3
+ All clients share the same auth header (Bearer plt_*) and error model
4
+ (:class:`PlurityAPIError`). Service-specific methods live on each subclass.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from typing import Any, Optional
11
+
12
+ import httpx
13
+
14
+ _DEFAULT_TIMEOUT = 30.0
15
+
16
+
17
+ class PlurityAPIError(Exception):
18
+ """Raised when a Plurity service returns an error response."""
19
+
20
+ def __init__(self, message: str, status_code: int | None = None) -> None:
21
+ super().__init__(message)
22
+ self.status_code = status_code
23
+
24
+
25
+ class _BaseClient:
26
+ """Shared httpx wrapper used by all service clients."""
27
+
28
+ def __init__(self, api_key: str, base_url: str, timeout: float = _DEFAULT_TIMEOUT) -> None:
29
+ self._base_url = base_url.rstrip("/")
30
+ self._client = httpx.Client(
31
+ base_url=self._base_url,
32
+ headers={
33
+ "Authorization": f"Bearer {api_key}",
34
+ "Content-Type": "application/json",
35
+ "Accept": "application/json",
36
+ },
37
+ timeout=timeout,
38
+ )
39
+
40
+ def _raise_for_status(self, response: httpx.Response) -> None:
41
+ if response.is_success:
42
+ return
43
+ try:
44
+ body = response.json()
45
+ detail = body.get("error") or body.get("message") or body.get("detail", "")
46
+ except Exception:
47
+ detail = response.text or "(no body)"
48
+ raise PlurityAPIError(
49
+ f"API error {response.status_code}: {detail}",
50
+ status_code=response.status_code,
51
+ )
52
+
53
+ def _get(self, path: str, **params: Any) -> dict[str, Any]:
54
+ response = self._client.get(
55
+ path, params={k: v for k, v in params.items() if v is not None}
56
+ )
57
+ self._raise_for_status(response)
58
+ return response.json()
59
+
60
+ def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
61
+ response = self._client.post(path, json=body)
62
+ self._raise_for_status(response)
63
+ return response.json()
64
+
65
+ def _patch(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
66
+ response = self._client.patch(path, json=body)
67
+ self._raise_for_status(response)
68
+ return response.json()
69
+
70
+ def _delete(self, path: str, **params: Any) -> dict[str, Any]:
71
+ response = self._client.delete(
72
+ path, params={k: v for k, v in params.items() if v is not None}
73
+ )
74
+ self._raise_for_status(response)
75
+ return response.json()
76
+
77
+ def close(self) -> None:
78
+ self._client.close()
79
+
80
+ def __enter__(self) -> "_BaseClient":
81
+ return self
82
+
83
+ def __exit__(self, *_: object) -> None:
84
+ self.close()
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Audit client
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ class AuditClient(_BaseClient):
93
+ """Client for the Plurity GEO Audit API."""
94
+
95
+ def submit_scan(self, url: str, webhook_url: str = "") -> dict[str, Any]:
96
+ body: dict[str, Any] = {"url": url}
97
+ if webhook_url:
98
+ body["webhook_url"] = webhook_url
99
+ return self._post("/api/v1/scans", body)
100
+
101
+ def get_scan(self, scan_id: str) -> dict[str, Any]:
102
+ return self._get(f"/api/v1/scans/{scan_id}")
103
+
104
+ def get_scan_by_url(self, url: str) -> dict[str, Any]:
105
+ return self._get("/api/v1/scans", url=url)
106
+
107
+ def wait_for_scan(
108
+ self,
109
+ scan_id: str,
110
+ timeout_seconds: int = 300,
111
+ poll_interval: float = 5.0,
112
+ ) -> dict[str, Any]:
113
+ _TERMINAL = {"complete", "failed"}
114
+ deadline = time.monotonic() + timeout_seconds
115
+ last: dict[str, Any] = {}
116
+ while time.monotonic() < deadline:
117
+ last = self.get_scan(scan_id)
118
+ if last.get("status") in _TERMINAL:
119
+ return last
120
+ remaining = deadline - time.monotonic()
121
+ time.sleep(min(poll_interval, max(remaining, 0)))
122
+ return last
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Toll client
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ class TollClient(_BaseClient):
131
+ """Client for the Plurity Toll API (agent traffic + llms.txt management)."""
132
+
133
+ # Sites
134
+ def list_sites(self) -> dict[str, Any]:
135
+ return self._get("/api/v1/sites")
136
+
137
+ def create_site(self, name: str, domain: str) -> dict[str, Any]:
138
+ return self._post("/api/v1/sites", {"name": name, "domain": domain})
139
+
140
+ def get_site(self, site_id: str) -> dict[str, Any]:
141
+ return self._get(f"/api/v1/sites/{site_id}")
142
+
143
+ def update_site(
144
+ self,
145
+ site_id: str,
146
+ name: Optional[str] = None,
147
+ domain: Optional[str] = None,
148
+ cache_ttl_secs: Optional[int] = None,
149
+ llms_txt_mode: Optional[str] = None,
150
+ ) -> dict[str, Any]:
151
+ body: dict[str, Any] = {}
152
+ if name is not None:
153
+ body["name"] = name
154
+ if domain is not None:
155
+ body["domain"] = domain
156
+ if cache_ttl_secs is not None:
157
+ body["cache_ttl_secs"] = cache_ttl_secs
158
+ if llms_txt_mode is not None:
159
+ body["llms_txt_mode"] = llms_txt_mode
160
+ return self._patch(f"/api/v1/sites/{site_id}", body)
161
+
162
+ # Q&A pairs
163
+ def list_qa_pairs(self, site_id: str) -> dict[str, Any]:
164
+ return self._get(f"/api/v1/sites/{site_id}/qa-pairs")
165
+
166
+ def create_qa_pair(
167
+ self,
168
+ site_id: str,
169
+ question: str,
170
+ answer_url: str,
171
+ answer_summary: Optional[str] = None,
172
+ ) -> dict[str, Any]:
173
+ body: dict[str, Any] = {"question": question, "answer_url": answer_url}
174
+ if answer_summary is not None:
175
+ body["answer_summary"] = answer_summary
176
+ return self._post(f"/api/v1/sites/{site_id}/qa-pairs", body)
177
+
178
+ def update_qa_pair(
179
+ self,
180
+ site_id: str,
181
+ pair_id: str,
182
+ question: Optional[str] = None,
183
+ answer_url: Optional[str] = None,
184
+ answer_summary: Optional[str] = None,
185
+ is_published: Optional[bool] = None,
186
+ ) -> dict[str, Any]:
187
+ body: dict[str, Any] = {}
188
+ if question is not None:
189
+ body["question"] = question
190
+ if answer_url is not None:
191
+ body["answer_url"] = answer_url
192
+ if answer_summary is not None:
193
+ body["answer_summary"] = answer_summary
194
+ if is_published is not None:
195
+ body["is_published"] = is_published
196
+ return self._patch(f"/api/v1/sites/{site_id}/qa-pairs/{pair_id}", body)
197
+
198
+ def delete_qa_pair(self, site_id: str, pair_id: str) -> dict[str, Any]:
199
+ return self._delete(f"/api/v1/sites/{site_id}/qa-pairs/{pair_id}")
200
+
201
+ # Traffic
202
+ def get_traffic(
203
+ self,
204
+ site_id: str,
205
+ period: str = "week",
206
+ agents: Optional[str] = None,
207
+ ) -> dict[str, Any]:
208
+ return self._get(
209
+ f"/api/v1/sites/{site_id}/events/chart",
210
+ period=period,
211
+ agents=agents,
212
+ )
213
+
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # Intelligence client
217
+ # ---------------------------------------------------------------------------
218
+
219
+
220
+ class IntelligenceClient(_BaseClient):
221
+ """Client for the Plurity Intelligence API (question + topic monitoring)."""
222
+
223
+ # Sources
224
+ def list_sources(
225
+ self,
226
+ type: Optional[str] = None,
227
+ q: Optional[str] = None,
228
+ ) -> dict[str, Any]:
229
+ return self._get("/api/v1/sources", type=type, q=q)
230
+
231
+ def list_subscriptions(self) -> dict[str, Any]:
232
+ return self._get("/api/v1/subscriptions")
233
+
234
+ def subscribe_source(
235
+ self,
236
+ source_id: str,
237
+ start_date: Optional[str] = None,
238
+ ) -> dict[str, Any]:
239
+ body: dict[str, Any] = {}
240
+ if start_date is not None:
241
+ body["start_date"] = start_date
242
+ return self._post(f"/api/v1/sources/{source_id}/subscribe", body)
243
+
244
+ def unsubscribe_source(self, source_id: str) -> dict[str, Any]:
245
+ return self._delete(f"/api/v1/sources/{source_id}/subscribe")
246
+
247
+ def request_source(self, url: str) -> dict[str, Any]:
248
+ return self._post("/api/v1/sources/request", {"url": url})
249
+
250
+ # Source content (documents)
251
+ def list_source_content(
252
+ self,
253
+ source_id: Optional[str] = None,
254
+ date_from: Optional[str] = None,
255
+ date_to: Optional[str] = None,
256
+ content_type: Optional[str] = None,
257
+ ) -> dict[str, Any]:
258
+ return self._get(
259
+ "/api/v1/source-content",
260
+ source_id=source_id,
261
+ date_from=date_from,
262
+ date_to=date_to,
263
+ content_type=content_type,
264
+ )
265
+
266
+ # Q&A feed
267
+ def list_qa_pairs(
268
+ self,
269
+ source_id: Optional[str] = None,
270
+ date_from: Optional[str] = None,
271
+ date_to: Optional[str] = None,
272
+ status: Optional[str] = None,
273
+ ) -> dict[str, Any]:
274
+ return self._get(
275
+ "/api/v1/feed",
276
+ source_id=source_id,
277
+ date_from=date_from,
278
+ date_to=date_to,
279
+ status=status,
280
+ )
281
+
282
+ def approve_qa_pair(self, qa_pair_id: str) -> dict[str, Any]:
283
+ return self._post(f"/api/v1/feed/{qa_pair_id}/approve", {})
284
+
285
+ def skip_qa_pair(self, qa_pair_id: str) -> dict[str, Any]:
286
+ return self._post(f"/api/v1/feed/{qa_pair_id}/skip", {})