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.
- plurity_mcp-0.1.0/.github/workflows/publish.yml +24 -0
- plurity_mcp-0.1.0/LICENSE +21 -0
- plurity_mcp-0.1.0/PKG-INFO +162 -0
- plurity_mcp-0.1.0/README.md +144 -0
- plurity_mcp-0.1.0/pyproject.toml +34 -0
- plurity_mcp-0.1.0/src/plurity_mcp/__init__.py +3 -0
- plurity_mcp-0.1.0/src/plurity_mcp/client.py +286 -0
- plurity_mcp-0.1.0/src/plurity_mcp/config.py +278 -0
- plurity_mcp-0.1.0/src/plurity_mcp/server.py +69 -0
- plurity_mcp-0.1.0/src/plurity_mcp/setup.py +118 -0
- plurity_mcp-0.1.0/src/plurity_mcp/tools/__init__.py +0 -0
- plurity_mcp-0.1.0/src/plurity_mcp/tools/audit.py +126 -0
- plurity_mcp-0.1.0/src/plurity_mcp/tools/intelligence.py +239 -0
- plurity_mcp-0.1.0/src/plurity_mcp/tools/toll.py +329 -0
|
@@ -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,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", {})
|