cloudrift-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.
- cloudrift_mcp-0.1.0/LICENSE +21 -0
- cloudrift_mcp-0.1.0/PKG-INFO +127 -0
- cloudrift_mcp-0.1.0/README.md +102 -0
- cloudrift_mcp-0.1.0/pyproject.toml +42 -0
- cloudrift_mcp-0.1.0/setup.cfg +4 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp/__init__.py +3 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp/client.py +102 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp/errors.py +19 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp/server.py +299 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp.egg-info/PKG-INFO +127 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp.egg-info/SOURCES.txt +16 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp.egg-info/dependency_links.txt +1 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp.egg-info/entry_points.txt +2 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp.egg-info/requires.txt +5 -0
- cloudrift_mcp-0.1.0/src/cloudrift_mcp.egg-info/top_level.txt +1 -0
- cloudrift_mcp-0.1.0/tests/test_errors.py +99 -0
- cloudrift_mcp-0.1.0/tests/test_server.py +63 -0
- cloudrift_mcp-0.1.0/tests/test_tools.py +287 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CloudRift
|
|
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,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloudrift-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only MCP server exposing CloudRift cloud-waste findings and verified-savings receipts to AI assistants
|
|
5
|
+
Author-email: CloudRift <support@cloudrift.tech>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://cloudrift.tech
|
|
8
|
+
Keywords: mcp,cloudrift,finops,cloud-cost,waste,savings
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: mcp<3,>=2
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# cloudrift-mcp
|
|
27
|
+
|
|
28
|
+
An [MCP](https://modelcontextprotocol.io) server that gives your AI
|
|
29
|
+
assistant (Claude Code, Claude Desktop, Cursor, and any other MCP
|
|
30
|
+
client) read-only access to your own
|
|
31
|
+
[CloudRift](https://cloudrift.tech) data: cloud-waste findings,
|
|
32
|
+
optimization recommendations, cost summaries, budgets, and the
|
|
33
|
+
verified-savings ledger. The thesis is simple: in the agent era, your
|
|
34
|
+
cost data should be queryable by the assistants you already work in -
|
|
35
|
+
"what are my ten most expensive orphaned resources?" or "how much has
|
|
36
|
+
CloudRift actually saved us, and how is that number computed?" should
|
|
37
|
+
be one question away, answered from your tenant's live findings and
|
|
38
|
+
receipts rather than a stale export.
|
|
39
|
+
|
|
40
|
+
Read-only by construction. Every tool is a GET against the CloudRift
|
|
41
|
+
public API, scoped to the tenant that owns the API key. No tool
|
|
42
|
+
mutates anything, and no tool accepts a credential argument - the key
|
|
43
|
+
comes from the environment only.
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/), or pip:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
uvx cloudrift-mcp # no install step
|
|
51
|
+
pip install cloudrift-mcp && cloudrift-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Create an API key in CloudRift (Settings > API Keys) with read-only
|
|
55
|
+
scopes, then export it as `CLOUDRIFT_API_KEY`.
|
|
56
|
+
|
|
57
|
+
### Claude Code
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
claude mcp add cloudrift --env CLOUDRIFT_API_KEY=crk_live_... -- uvx cloudrift-mcp
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Claude Desktop
|
|
64
|
+
|
|
65
|
+
Add to `claude_desktop_config.json` (Settings > Developer > Edit
|
|
66
|
+
Config):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"cloudrift": {
|
|
72
|
+
"command": "uvx",
|
|
73
|
+
"args": ["cloudrift-mcp"],
|
|
74
|
+
"env": {
|
|
75
|
+
"CLOUDRIFT_API_KEY": "crk_live_..."
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Environment variables
|
|
83
|
+
|
|
84
|
+
| Variable | Required | Meaning |
|
|
85
|
+
| ------------------- | -------- | -------------------------------------------------- |
|
|
86
|
+
| `CLOUDRIFT_API_KEY` | yes | A CloudRift API key. Without it the server starts, but every tool returns a clear CONFIG error. |
|
|
87
|
+
| `CLOUDRIFT_API_URL` | no | API origin; defaults to `https://cloudrift.tech`. |
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
All tools are read-only GETs with a 30 second timeout. Lists are
|
|
92
|
+
capped, and every capped result states the cap and the full totals, so
|
|
93
|
+
a partial view is never silent.
|
|
94
|
+
|
|
95
|
+
| Tool | Arguments | Endpoint | Returns |
|
|
96
|
+
| --------------------- | --------------------------- | ------------------------- | ------- |
|
|
97
|
+
| `list_waste_findings` | `account_id?`, `limit?` (default 25, max 100) | `/api/v1/resources` | Waste findings: orphaned resources first, then optimization candidates, monthly cost descending, with full waste totals. |
|
|
98
|
+
| `get_recommendations` | `account_id?` (filtered locally) | `/api/v1/recommendations` | Optimization recommendations, estimated monthly savings descending (top 50), with the summed savings across all matches. |
|
|
99
|
+
| `get_cost_summary` | - | `/api/v1/costs/summary` | Monthly spend, orphaned (waste) cost, annual savings potential, per-account breakdown, latest scan date. |
|
|
100
|
+
| `get_savings_receipts`| - | `/api/v1/receipts` | The verified-savings ledger: entries labelled estimated / confirmed / billing_verified, per-status totals, and the API's `basis` string verbatim (how returned_to_date is computed). Requires CloudRift >= 2026-09-30. |
|
|
101
|
+
| `get_budgets` | - | `/api/v1/budgets` | Configured spend budgets with warning / critical thresholds. |
|
|
102
|
+
|
|
103
|
+
Errors come back as one honest line - `API_ERROR: CloudRift API
|
|
104
|
+
returned 403 for GET /api/v1/costs/summary: This API key is missing
|
|
105
|
+
the required scope: read:costs.` - never a stack trace.
|
|
106
|
+
|
|
107
|
+
## Security
|
|
108
|
+
|
|
109
|
+
- **Use a read-only key.** Grant only the `read:*` scopes
|
|
110
|
+
(`read:resources`, `read:recommendations`, `read:costs`,
|
|
111
|
+
`read:budgets`) when you mint the key. This server only ever issues
|
|
112
|
+
GETs, so a broader key buys nothing and risks more.
|
|
113
|
+
- **Env-only auth.** The key is read from `CLOUDRIFT_API_KEY` and sent
|
|
114
|
+
as `Authorization: Bearer`. No tool accepts a key as an argument (a
|
|
115
|
+
test enforces that no credential-shaped field exists in any tool
|
|
116
|
+
schema), and the key is never echoed into tool results or error
|
|
117
|
+
messages.
|
|
118
|
+
- **Tenant-scoped by the API.** The CloudRift public API scopes every
|
|
119
|
+
query to the key owner's account; this server adds no cross-tenant
|
|
120
|
+
reach.
|
|
121
|
+
- **Bounded requests.** At most five pages of 200 items are fetched
|
|
122
|
+
per tool call, and CloudRift rate-limits each key to 120
|
|
123
|
+
requests/minute.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# cloudrift-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that gives your AI
|
|
4
|
+
assistant (Claude Code, Claude Desktop, Cursor, and any other MCP
|
|
5
|
+
client) read-only access to your own
|
|
6
|
+
[CloudRift](https://cloudrift.tech) data: cloud-waste findings,
|
|
7
|
+
optimization recommendations, cost summaries, budgets, and the
|
|
8
|
+
verified-savings ledger. The thesis is simple: in the agent era, your
|
|
9
|
+
cost data should be queryable by the assistants you already work in -
|
|
10
|
+
"what are my ten most expensive orphaned resources?" or "how much has
|
|
11
|
+
CloudRift actually saved us, and how is that number computed?" should
|
|
12
|
+
be one question away, answered from your tenant's live findings and
|
|
13
|
+
receipts rather than a stale export.
|
|
14
|
+
|
|
15
|
+
Read-only by construction. Every tool is a GET against the CloudRift
|
|
16
|
+
public API, scoped to the tenant that owns the API key. No tool
|
|
17
|
+
mutates anything, and no tool accepts a credential argument - the key
|
|
18
|
+
comes from the environment only.
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/), or pip:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
uvx cloudrift-mcp # no install step
|
|
26
|
+
pip install cloudrift-mcp && cloudrift-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Create an API key in CloudRift (Settings > API Keys) with read-only
|
|
30
|
+
scopes, then export it as `CLOUDRIFT_API_KEY`.
|
|
31
|
+
|
|
32
|
+
### Claude Code
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
claude mcp add cloudrift --env CLOUDRIFT_API_KEY=crk_live_... -- uvx cloudrift-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Claude Desktop
|
|
39
|
+
|
|
40
|
+
Add to `claude_desktop_config.json` (Settings > Developer > Edit
|
|
41
|
+
Config):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"cloudrift": {
|
|
47
|
+
"command": "uvx",
|
|
48
|
+
"args": ["cloudrift-mcp"],
|
|
49
|
+
"env": {
|
|
50
|
+
"CLOUDRIFT_API_KEY": "crk_live_..."
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Environment variables
|
|
58
|
+
|
|
59
|
+
| Variable | Required | Meaning |
|
|
60
|
+
| ------------------- | -------- | -------------------------------------------------- |
|
|
61
|
+
| `CLOUDRIFT_API_KEY` | yes | A CloudRift API key. Without it the server starts, but every tool returns a clear CONFIG error. |
|
|
62
|
+
| `CLOUDRIFT_API_URL` | no | API origin; defaults to `https://cloudrift.tech`. |
|
|
63
|
+
|
|
64
|
+
## Tools
|
|
65
|
+
|
|
66
|
+
All tools are read-only GETs with a 30 second timeout. Lists are
|
|
67
|
+
capped, and every capped result states the cap and the full totals, so
|
|
68
|
+
a partial view is never silent.
|
|
69
|
+
|
|
70
|
+
| Tool | Arguments | Endpoint | Returns |
|
|
71
|
+
| --------------------- | --------------------------- | ------------------------- | ------- |
|
|
72
|
+
| `list_waste_findings` | `account_id?`, `limit?` (default 25, max 100) | `/api/v1/resources` | Waste findings: orphaned resources first, then optimization candidates, monthly cost descending, with full waste totals. |
|
|
73
|
+
| `get_recommendations` | `account_id?` (filtered locally) | `/api/v1/recommendations` | Optimization recommendations, estimated monthly savings descending (top 50), with the summed savings across all matches. |
|
|
74
|
+
| `get_cost_summary` | - | `/api/v1/costs/summary` | Monthly spend, orphaned (waste) cost, annual savings potential, per-account breakdown, latest scan date. |
|
|
75
|
+
| `get_savings_receipts`| - | `/api/v1/receipts` | The verified-savings ledger: entries labelled estimated / confirmed / billing_verified, per-status totals, and the API's `basis` string verbatim (how returned_to_date is computed). Requires CloudRift >= 2026-09-30. |
|
|
76
|
+
| `get_budgets` | - | `/api/v1/budgets` | Configured spend budgets with warning / critical thresholds. |
|
|
77
|
+
|
|
78
|
+
Errors come back as one honest line - `API_ERROR: CloudRift API
|
|
79
|
+
returned 403 for GET /api/v1/costs/summary: This API key is missing
|
|
80
|
+
the required scope: read:costs.` - never a stack trace.
|
|
81
|
+
|
|
82
|
+
## Security
|
|
83
|
+
|
|
84
|
+
- **Use a read-only key.** Grant only the `read:*` scopes
|
|
85
|
+
(`read:resources`, `read:recommendations`, `read:costs`,
|
|
86
|
+
`read:budgets`) when you mint the key. This server only ever issues
|
|
87
|
+
GETs, so a broader key buys nothing and risks more.
|
|
88
|
+
- **Env-only auth.** The key is read from `CLOUDRIFT_API_KEY` and sent
|
|
89
|
+
as `Authorization: Bearer`. No tool accepts a key as an argument (a
|
|
90
|
+
test enforces that no credential-shaped field exists in any tool
|
|
91
|
+
schema), and the key is never echoed into tool results or error
|
|
92
|
+
messages.
|
|
93
|
+
- **Tenant-scoped by the API.** The CloudRift public API scopes every
|
|
94
|
+
query to the key owner's account; this server adds no cross-tenant
|
|
95
|
+
reach.
|
|
96
|
+
- **Bounded requests.** At most five pages of 200 items are fetched
|
|
97
|
+
per tool call, and CloudRift rate-limits each key to 120
|
|
98
|
+
requests/minute.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cloudrift-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read-only MCP server exposing CloudRift cloud-waste findings and verified-savings receipts to AI assistants"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "CloudRift", email = "support@cloudrift.tech" }]
|
|
13
|
+
keywords = ["mcp", "cloudrift", "finops", "cloud-cost", "waste", "savings"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"mcp>=2,<3",
|
|
26
|
+
"httpx>=0.27",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=8"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://cloudrift.tech"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
cloudrift-mcp = "cloudrift_mcp.server:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""HTTP access to the CloudRift public API (read-only GETs).
|
|
2
|
+
|
|
3
|
+
Configuration comes from the environment ONLY:
|
|
4
|
+
|
|
5
|
+
CLOUDRIFT_API_KEY required. A CloudRift API key (crk_live_...).
|
|
6
|
+
Without it the server still starts, but every
|
|
7
|
+
tool returns a clear CONFIG error.
|
|
8
|
+
CLOUDRIFT_API_URL optional. Defaults to https://cloudrift.tech.
|
|
9
|
+
|
|
10
|
+
The key is sent as `Authorization: Bearer <key>`. It is never accepted
|
|
11
|
+
as a tool argument and never echoed into any tool result or error
|
|
12
|
+
message. All requests are GETs with a 30 second timeout; failures are
|
|
13
|
+
returned as honest one-line tool errors (status + the API's own
|
|
14
|
+
detail), never stack traces.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from typing import Any, Dict, Optional
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from .errors import API_ERROR, CONFIG, NETWORK_ERROR, tool_error
|
|
25
|
+
|
|
26
|
+
DEFAULT_API_URL = "https://cloudrift.tech"
|
|
27
|
+
TIMEOUT_SECONDS = 30.0
|
|
28
|
+
|
|
29
|
+
# Tests set this to an httpx.MockTransport; production leaves it None.
|
|
30
|
+
transport: Optional[httpx.BaseTransport] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def base_url() -> str:
|
|
34
|
+
"""The API origin, from CLOUDRIFT_API_URL, without a trailing slash."""
|
|
35
|
+
return (os.environ.get("CLOUDRIFT_API_URL") or DEFAULT_API_URL).rstrip("/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _api_key() -> str:
|
|
39
|
+
key = (os.environ.get("CLOUDRIFT_API_KEY") or "").strip()
|
|
40
|
+
if not key:
|
|
41
|
+
raise tool_error(
|
|
42
|
+
CONFIG,
|
|
43
|
+
"CLOUDRIFT_API_KEY is not set. Create a read-only API key in "
|
|
44
|
+
"CloudRift (Settings > API Keys) and export it in the "
|
|
45
|
+
"environment of this MCP server; keys are never accepted as "
|
|
46
|
+
"tool arguments.")
|
|
47
|
+
return key
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get(path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
51
|
+
"""GET an /api/v1 path and return the parsed JSON body.
|
|
52
|
+
|
|
53
|
+
`path` is the part after the origin, e.g. "/api/v1/resources".
|
|
54
|
+
Raises a ToolError (CONFIG / NETWORK_ERROR / API_ERROR) on any
|
|
55
|
+
failure; the message carries the status and the API's own `detail`
|
|
56
|
+
when there is one.
|
|
57
|
+
"""
|
|
58
|
+
url = base_url() + path
|
|
59
|
+
headers = {"Authorization": "Bearer " + _api_key()}
|
|
60
|
+
try:
|
|
61
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as http:
|
|
62
|
+
response = http.get(url, params=params or {}, headers=headers)
|
|
63
|
+
except httpx.TimeoutException:
|
|
64
|
+
raise tool_error(
|
|
65
|
+
NETWORK_ERROR,
|
|
66
|
+
f"GET {path} timed out after {int(TIMEOUT_SECONDS)}s "
|
|
67
|
+
f"({base_url()}).") from None
|
|
68
|
+
except httpx.HTTPError as exc:
|
|
69
|
+
# One honest line (e.g. connection refused), never a traceback.
|
|
70
|
+
raise tool_error(
|
|
71
|
+
NETWORK_ERROR,
|
|
72
|
+
f"could not reach {base_url()}: {exc.__class__.__name__}: "
|
|
73
|
+
f"{exc}") from None
|
|
74
|
+
|
|
75
|
+
if response.status_code >= 400:
|
|
76
|
+
raise tool_error(
|
|
77
|
+
API_ERROR,
|
|
78
|
+
f"CloudRift API returned {response.status_code} for GET {path}: "
|
|
79
|
+
f"{_api_detail(response)}")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
return response.json()
|
|
83
|
+
except ValueError:
|
|
84
|
+
raise tool_error(
|
|
85
|
+
API_ERROR,
|
|
86
|
+
f"CloudRift API returned a non-JSON body for GET {path} "
|
|
87
|
+
f"(status {response.status_code}).") from None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _api_detail(response: httpx.Response) -> str:
|
|
91
|
+
"""The API's own `detail` string, else a truncated body, else the
|
|
92
|
+
HTTP reason phrase."""
|
|
93
|
+
try:
|
|
94
|
+
body = response.json()
|
|
95
|
+
if isinstance(body, dict) and body.get("detail"):
|
|
96
|
+
return str(body["detail"])
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass
|
|
99
|
+
text = (response.text or "").strip()
|
|
100
|
+
if text:
|
|
101
|
+
return text[:200]
|
|
102
|
+
return response.reason_phrase or "no detail provided"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Structured tool errors.
|
|
2
|
+
|
|
3
|
+
Every failure a tool can hit is raised as a ToolError whose message
|
|
4
|
+
starts with a stable code, so an agent (or a human reading an editor
|
|
5
|
+
pane) gets "CODE: one clear sentence" and never a stack trace. The MCP
|
|
6
|
+
SDK converts a raised ToolError into an is_error tool result.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
10
|
+
|
|
11
|
+
CONFIG = "CONFIG"
|
|
12
|
+
INVALID_ARGUMENT = "INVALID_ARGUMENT"
|
|
13
|
+
API_ERROR = "API_ERROR"
|
|
14
|
+
NETWORK_ERROR = "NETWORK_ERROR"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def tool_error(code: str, message: str) -> ToolError:
|
|
18
|
+
"""Build (not raise) a ToolError with the standard CODE: message shape."""
|
|
19
|
+
return ToolError(f"{code}: {message}")
|