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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """cloudrift-mcp: read-only MCP access to a CloudRift tenant's own data."""
2
+
3
+ __version__ = "0.1.0"
@@ -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}")