cleanlib-mcp-server 0.2.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,15 @@
1
+ CleanStart Inc Proprietary License
2
+
3
+ Copyright (c) 2026 CleanStart Inc. All rights reserved.
4
+
5
+ This software is the proprietary property of CleanStart Inc. Use is permitted
6
+ only under the terms of a separate commercial license agreement with CleanStart
7
+ Inc. or its authorized distributors. Unauthorized use, modification, distribution,
8
+ or reverse engineering is prohibited.
9
+
10
+ For licensing inquiries: cto.office@cleanstart.com
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
14
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: cleanlib-mcp-server
3
+ Version: 0.2.0
4
+ Summary: CleanLibrary MCP server — exposes verdict-aware supply-chain risk assessment as Model Context Protocol tools for AI agent workflows
5
+ Author-email: CleanStart Inc <cto.office@cleanstart.com>
6
+ Maintainer-email: CleanStart Inc <cto.office@cleanstart.com>
7
+ License: CleanStart Inc Proprietary
8
+ Project-URL: Homepage, https://cleanlibrary.clnstrt.dev
9
+ Project-URL: Documentation, https://cleanlibrary.clnstrt.dev
10
+ Keywords: cleanlibrary,mcp,model-context-protocol,cleanstart,supply-chain-security,verdict,policy-evaluation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: License :: Other/Proprietary License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Software Distribution
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: mcp>=1.0.0
28
+ Requires-Dist: httpx>=0.27.0
29
+ Requires-Dist: pydantic>=2.7.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
32
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # cleanlib-mcp-server
37
+
38
+ CleanLibrary MCP (Model Context Protocol) server — expose verdict-aware supply-chain risk assessment as MCP tools, so MCP-capable clients (Claude Code, Claude Desktop, Cursor, GitHub Copilot, and other agents) can fetch package verdicts directly inside the developer's workflow.
39
+
40
+ Ask your AI assistant *"is cors@2.8.4 safe to install?"* and it queries CleanLibrary for an `ALLOW` / `DENY` / `WARN` verdict with reasoning and confidence — without leaving the editor.
41
+
42
+ ## Tools
43
+
44
+ | Tool | Description |
45
+ |---|---|
46
+ | `cleanlib_fetch_verdict(ecosystem, package, version)` | Fetch a verdict (`ALLOW` / `DENY` / `WARN`) with reasoning and confidence for a package version |
47
+ | `cleanlib_health_check()` | Report server status + whether a live CleanLibrary backend is configured |
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install cleanlib-mcp-server
53
+ ```
54
+
55
+ ## Run
56
+
57
+ ```bash
58
+ cleanlib-mcp-server # stdio transport (per MCP spec)
59
+ ```
60
+
61
+ ## Backend modes
62
+
63
+ - **Connected** — when `CLEANLIB_ENDPOINT` + `CLEANLIB_API_KEY` are set, the server queries your CleanLibrary deployment for live verdicts.
64
+ - **Local fixtures** — when no endpoint is configured (or the configured endpoint is unreachable), the server returns bundled demo fixtures so MCP clients always receive useful output.
65
+
66
+ ## MCP client integration
67
+
68
+ The server speaks standard MCP over stdio, so it works with any MCP-capable client. Example configuration (Claude Desktop — `~/Library/Application Support/Claude/claude_desktop_config.json`; other clients use the same `mcpServers` shape):
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "cleanlibrary": {
74
+ "command": "cleanlib-mcp-server"
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ To connect a live CleanLibrary backend, add the endpoint + API key:
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "cleanlibrary": {
86
+ "command": "cleanlib-mcp-server",
87
+ "env": {
88
+ "CLEANLIB_ENDPOINT": "https://cleanapp.clnstrt.dev",
89
+ "CLEANLIB_API_KEY": "clk_..."
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ The same `command` + `env` pattern applies to Cursor, GitHub Copilot, and other MCP clients — consult your client's MCP server configuration docs for the exact file location.
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ python -m venv .venv && source .venv/bin/activate
102
+ pip install -e ".[dev]"
103
+ ruff check src tests
104
+ pytest -v
105
+ ```
106
+
107
+ ## License
108
+
109
+ Proprietary. See [LICENSE](./LICENSE) for terms. © 2026 CleanStart Inc.
@@ -0,0 +1,74 @@
1
+ # cleanlib-mcp-server
2
+
3
+ CleanLibrary MCP (Model Context Protocol) server — expose verdict-aware supply-chain risk assessment as MCP tools, so MCP-capable clients (Claude Code, Claude Desktop, Cursor, GitHub Copilot, and other agents) can fetch package verdicts directly inside the developer's workflow.
4
+
5
+ Ask your AI assistant *"is cors@2.8.4 safe to install?"* and it queries CleanLibrary for an `ALLOW` / `DENY` / `WARN` verdict with reasoning and confidence — without leaving the editor.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Description |
10
+ |---|---|
11
+ | `cleanlib_fetch_verdict(ecosystem, package, version)` | Fetch a verdict (`ALLOW` / `DENY` / `WARN`) with reasoning and confidence for a package version |
12
+ | `cleanlib_health_check()` | Report server status + whether a live CleanLibrary backend is configured |
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install cleanlib-mcp-server
18
+ ```
19
+
20
+ ## Run
21
+
22
+ ```bash
23
+ cleanlib-mcp-server # stdio transport (per MCP spec)
24
+ ```
25
+
26
+ ## Backend modes
27
+
28
+ - **Connected** — when `CLEANLIB_ENDPOINT` + `CLEANLIB_API_KEY` are set, the server queries your CleanLibrary deployment for live verdicts.
29
+ - **Local fixtures** — when no endpoint is configured (or the configured endpoint is unreachable), the server returns bundled demo fixtures so MCP clients always receive useful output.
30
+
31
+ ## MCP client integration
32
+
33
+ The server speaks standard MCP over stdio, so it works with any MCP-capable client. Example configuration (Claude Desktop — `~/Library/Application Support/Claude/claude_desktop_config.json`; other clients use the same `mcpServers` shape):
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "cleanlibrary": {
39
+ "command": "cleanlib-mcp-server"
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ To connect a live CleanLibrary backend, add the endpoint + API key:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "cleanlibrary": {
51
+ "command": "cleanlib-mcp-server",
52
+ "env": {
53
+ "CLEANLIB_ENDPOINT": "https://cleanapp.clnstrt.dev",
54
+ "CLEANLIB_API_KEY": "clk_..."
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ The same `command` + `env` pattern applies to Cursor, GitHub Copilot, and other MCP clients — consult your client's MCP server configuration docs for the exact file location.
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ python -m venv .venv && source .venv/bin/activate
67
+ pip install -e ".[dev]"
68
+ ruff check src tests
69
+ pytest -v
70
+ ```
71
+
72
+ ## License
73
+
74
+ Proprietary. See [LICENSE](./LICENSE) for terms. © 2026 CleanStart Inc.
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cleanlib-mcp-server"
7
+ version = "0.2.0"
8
+ description = "CleanLibrary MCP server — exposes verdict-aware supply-chain risk assessment as Model Context Protocol tools for AI agent workflows"
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ requires-python = ">=3.11"
11
+ license = { text = "CleanStart Inc Proprietary" }
12
+ authors = [{ name = "CleanStart Inc", email = "cto.office@cleanstart.com" }]
13
+ maintainers = [{ name = "CleanStart Inc", email = "cto.office@cleanstart.com" }]
14
+ keywords = ["cleanlibrary", "mcp", "model-context-protocol", "cleanstart", "supply-chain-security", "verdict", "policy-evaluation"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Information Technology",
19
+ "License :: Other/Proprietary License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Security",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Topic :: System :: Software Distribution",
29
+ ]
30
+
31
+ dependencies = [
32
+ "mcp>=1.0.0",
33
+ "httpx>=0.27.0",
34
+ "pydantic>=2.7.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://cleanlibrary.clnstrt.dev"
39
+ Documentation = "https://cleanlibrary.clnstrt.dev"
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=8.0.0",
44
+ "pytest-asyncio>=0.23.0",
45
+ "ruff>=0.6.0",
46
+ ]
47
+
48
+ [project.scripts]
49
+ cleanlib-mcp-server = "cleanlib_mcp.server:main"
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
54
+ [tool.pytest.ini_options]
55
+ asyncio_mode = "auto"
56
+ testpaths = ["tests"]
57
+
58
+ [tool.ruff]
59
+ line-length = 100
60
+ target-version = "py311"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """CleanLibrary MCP server.
2
+
3
+ Exposes CleanLibrary verdict-aware supply-chain risk assessment as MCP tools
4
+ so MCP-capable clients (Claude Code, Claude Desktop, Cursor, GitHub Copilot,
5
+ and other agents) can fetch package verdicts directly.
6
+ """
7
+
8
+ __version__ = "0.2.0"
@@ -0,0 +1,221 @@
1
+ """Verdict backend.
2
+
3
+ When CLEANLIB_ENDPOINT + CLEANLIB_API_KEY are configured, fetches verdicts
4
+ from the CleanLibrary App `GET /v1/customer/verdicts/{eco}/{pkg}/{ver}`.
5
+ When unconfigured (or the endpoint is unreachable), returns bundled demo
6
+ fixtures so MCP clients always receive useful output.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import secrets
13
+ from dataclasses import asdict, dataclass
14
+ from typing import Literal
15
+
16
+ import httpx
17
+
18
+ Decision = Literal["ALLOW", "DENY", "WARN"]
19
+
20
+
21
+ @dataclass
22
+ class Verdict:
23
+ """Mirrors the CleanLibrary App GET /v1/customer/verdicts response shape."""
24
+
25
+ verdict_id: str
26
+ ecosystem: str
27
+ package: str
28
+ version: str
29
+ decision: Decision
30
+ reasoning: str | None = None
31
+ confidence: float | None = None
32
+ source: str | None = None
33
+
34
+ def to_dict(self) -> dict:
35
+ return {k: v for k, v in asdict(self).items() if v is not None}
36
+
37
+
38
+ # Bundled demo fixtures (cors-2.8.4 DENY / cors-2.8.5 ALLOW / helmet ALLOW /
39
+ # dotenv ALLOW / oracledb WARN) used when no live backend is configured.
40
+ _MOCK_FIXTURES: dict[str, dict] = {
41
+ "npm/cors/2.8.4": {
42
+ "decision": "DENY",
43
+ "reasoning": "Policy rule: cors-2.8.4 has known prototype pollution; upgrade to 2.8.5",
44
+ "confidence": 0.98,
45
+ "source": "VECTOR_VERDICT",
46
+ },
47
+ "npm/cors/2.8.5": {
48
+ "decision": "ALLOW",
49
+ "reasoning": "No findings; matches policy allowlist",
50
+ "confidence": 0.95,
51
+ "source": "ALLOWED_NO_FINDINGS",
52
+ },
53
+ "npm/helmet/8.0.0": {
54
+ "decision": "ALLOW",
55
+ "reasoning": "No findings; standard security middleware",
56
+ "confidence": 0.97,
57
+ "source": "ALLOWED_NO_FINDINGS",
58
+ },
59
+ "npm/dotenv/16.4.5": {
60
+ "decision": "ALLOW",
61
+ "reasoning": "No findings; widely-used config loader",
62
+ "confidence": 0.94,
63
+ "source": "ALLOWED_NO_FINDINGS",
64
+ },
65
+ "npm/oracledb/6.5.1": {
66
+ "decision": "WARN",
67
+ "reasoning": "Vendor-binary present; review supply-chain provenance before deploy",
68
+ "confidence": 0.70,
69
+ "source": "INSUFFICIENT_DATA",
70
+ },
71
+ }
72
+
73
+
74
+ async def fetch_verdict(ecosystem: str, package: str, version: str) -> Verdict:
75
+ """Fetch verdict for a package version.
76
+
77
+ Fixture-mode (default): returns a bundled demo fixture, or default-ALLOW
78
+ for unknown packages.
79
+
80
+ Live-mode (when CLEANLIB_ENDPOINT + CLEANLIB_API_KEY env set): queries the
81
+ CleanLibrary App customer-verdict endpoint, falling back to a fixture if
82
+ the endpoint is unreachable so the client always receives output.
83
+ """
84
+ endpoint = os.getenv("CLEANLIB_ENDPOINT", "").strip()
85
+ api_key = os.getenv("CLEANLIB_API_KEY", "").strip()
86
+
87
+ if endpoint and api_key:
88
+ try:
89
+ return await _live_fetch(endpoint, api_key, ecosystem, package, version)
90
+ except LiveBackendNotDeployed:
91
+ # Configured endpoint unreachable — fall back to a fixture so the
92
+ # client still receives useful output. The fallback verdict carries
93
+ # a reasoning string surfacing that the live backend was unavailable.
94
+ v = _mock_fetch(ecosystem, package, version)
95
+ v.reasoning = (
96
+ f"[live-mode fallback: CleanLibrary backend at {endpoint} unavailable] "
97
+ + (v.reasoning or "")
98
+ )
99
+ return v
100
+ return _mock_fetch(ecosystem, package, version)
101
+
102
+
103
+ def _mock_fetch(ecosystem: str, package: str, version: str) -> Verdict:
104
+ key = f"{ecosystem}/{package}/{version}"
105
+ verdict_id = f"mock_{secrets.token_hex(4)}"
106
+ fixture = _MOCK_FIXTURES.get(key)
107
+ if fixture is not None:
108
+ return Verdict(
109
+ verdict_id=verdict_id,
110
+ ecosystem=ecosystem,
111
+ package=package,
112
+ version=version,
113
+ **fixture,
114
+ )
115
+ return Verdict(
116
+ verdict_id=verdict_id,
117
+ ecosystem=ecosystem,
118
+ package=package,
119
+ version=version,
120
+ decision="ALLOW",
121
+ reasoning="Default-ALLOW (no fixture matched; live App returned no verdict for this package version)",
122
+ confidence=0.5,
123
+ source="INSUFFICIENT_DATA",
124
+ )
125
+
126
+
127
+ class LiveBackendNotDeployed(Exception):
128
+ """Raised when CLEANLIB_ENDPOINT is set but the verdict endpoint is
129
+ unreachable (empty-body 404). Distinguished from "package has no verdict"
130
+ (a structured-JSON 404) by response content-type. The tool layer treats
131
+ this as a degraded-mode signal and falls back to a fixture so clients
132
+ receive useful output even when the live backend is unavailable."""
133
+
134
+
135
+ async def _live_fetch(
136
+ endpoint: str, api_key: str, ecosystem: str, package: str, version: str
137
+ ) -> Verdict:
138
+ """Live `GET /v1/customer/verdicts/{eco}/{pkg}/{ver}` call against the
139
+ configured CleanLibrary App.
140
+
141
+ A structured-JSON 404 means "verdict not found" for that package version;
142
+ an empty-body 404 means the endpoint is unreachable. The wrapper
143
+ distinguishes the two by response content-type and falls back to a
144
+ fixture in the unreachable case.
145
+ """
146
+ url = f"{endpoint.rstrip('/')}/v1/customer/verdicts/{ecosystem}/{package}/{version}"
147
+ headers = {
148
+ "authorization": f"Bearer {api_key}",
149
+ "accept": "application/json",
150
+ "user-agent": "cleanlib-mcp-server/0.2.0",
151
+ }
152
+ async with httpx.AsyncClient(timeout=30.0) as client:
153
+ try:
154
+ resp = await client.get(url, headers=headers)
155
+ except httpx.HTTPError as e:
156
+ raise RuntimeError(f"live App transport failure: {e}") from e
157
+
158
+ if resp.status_code == 200:
159
+ data = resp.json()
160
+ return Verdict(
161
+ verdict_id=data.get("verdict_id", ""),
162
+ ecosystem=data.get("ecosystem", ecosystem),
163
+ package=data.get("package", package),
164
+ version=data.get("version", version),
165
+ decision=data.get("decision", "ALLOW"),
166
+ reasoning=data.get("reasoning"),
167
+ confidence=data.get("confidence"),
168
+ source=data.get("source"),
169
+ )
170
+
171
+ if resp.status_code == 404:
172
+ # Two cases: (a) endpoint reached but no verdict for this package
173
+ # (structured-JSON body); (b) endpoint unreachable (empty body or
174
+ # text/html). Distinguish via content-type.
175
+ content_type = resp.headers.get("content-type", "")
176
+ if "json" in content_type and resp.content:
177
+ # "Verdict not found" — return an INSUFFICIENT_DATA verdict.
178
+ return Verdict(
179
+ verdict_id=f"live_404_{secrets.token_hex(4)}",
180
+ ecosystem=ecosystem,
181
+ package=package,
182
+ version=version,
183
+ decision="ALLOW",
184
+ reasoning=f"No verdict on file for {ecosystem}/{package}@{version}",
185
+ confidence=None,
186
+ source="INSUFFICIENT_DATA",
187
+ )
188
+ raise LiveBackendNotDeployed(
189
+ f"CleanLibrary backend at {endpoint} did not return a verdict for "
190
+ f"/v1/customer/verdicts/{ecosystem}/{package}/{version}; "
191
+ f"returning fixture fallback."
192
+ )
193
+
194
+ if resp.status_code == 401 or resp.status_code == 403:
195
+ raise PermissionError(
196
+ f"App rejected Bearer (status={resp.status_code}); check CLEANLIB_API_KEY "
197
+ f"is valid for {endpoint}"
198
+ )
199
+
200
+ raise RuntimeError(f"App returned unexpected status {resp.status_code}: {resp.text[:200]}")
201
+
202
+
203
+ async def _validate_live_endpoint_reachable(endpoint: str) -> bool:
204
+ """Reachability probe via /health (public; no auth required). Used by
205
+ `cleanlib_health_check` MCP tool to verify MCP→App transport works
206
+ end-to-end even when the verdict endpoint isn't yet deployed."""
207
+ try:
208
+ async with httpx.AsyncClient(timeout=5.0) as client:
209
+ resp = await client.get(f"{endpoint.rstrip('/')}/health")
210
+ return resp.status_code == 200
211
+ except Exception:
212
+ return False
213
+
214
+
215
+ async def live_health(endpoint: str) -> dict:
216
+ """Fetch the App's /health response (JSON) — used by the
217
+ `cleanlib_health_check` MCP tool. Public endpoint; no auth needed."""
218
+ async with httpx.AsyncClient(timeout=10.0) as client:
219
+ resp = await client.get(f"{endpoint.rstrip('/')}/health")
220
+ resp.raise_for_status()
221
+ return resp.json()
@@ -0,0 +1,128 @@
1
+ """MCP server entry-point — registers CleanLibrary tools and serves over
2
+ stdio transport. Compatible with any MCP-capable client (Claude Code,
3
+ Claude Desktop, Cursor, GitHub Copilot, and other agents).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import logging
10
+
11
+ from mcp.server import Server
12
+ from mcp.server.stdio import stdio_server
13
+ from mcp.types import TextContent, Tool
14
+
15
+ from .backend import fetch_verdict, live_health
16
+
17
+ log = logging.getLogger("cleanlib-mcp-server")
18
+
19
+ server: Server = Server("cleanlib-mcp-server")
20
+
21
+
22
+ @server.list_tools()
23
+ async def list_tools() -> list[Tool]:
24
+ return [
25
+ Tool(
26
+ name="cleanlib_fetch_verdict",
27
+ description=(
28
+ "Fetch the CleanLibrary verdict for a package version. Returns the "
29
+ "decision (ALLOW / DENY / WARN), reasoning, and confidence. Queries a "
30
+ "live CleanLibrary backend when CLEANLIB_ENDPOINT + CLEANLIB_API_KEY "
31
+ "are configured; otherwise returns bundled demo fixtures so the tool "
32
+ "always responds."
33
+ ),
34
+ inputSchema={
35
+ "type": "object",
36
+ "properties": {
37
+ "ecosystem": {
38
+ "type": "string",
39
+ "enum": ["npm", "pypi", "go", "maven", "crates", "nuget", "rubygems"],
40
+ "description": "Package ecosystem",
41
+ },
42
+ "package": {
43
+ "type": "string",
44
+ "description": "Package name (e.g. lodash, requests, github.com/spf13/cobra)",
45
+ },
46
+ "version": {
47
+ "type": "string",
48
+ "description": "Semantic version (e.g. 4.17.21)",
49
+ },
50
+ },
51
+ "required": ["ecosystem", "package", "version"],
52
+ },
53
+ ),
54
+ Tool(
55
+ name="cleanlib_health_check",
56
+ description=(
57
+ "Verify MCP→App transport reachability via the public /health "
58
+ "endpoint. Returns the App's health JSON ({status, service, version, "
59
+ "ecosystems_mounted}). Requires CLEANLIB_ENDPOINT env var. Public "
60
+ "endpoint; no API key needed. Use this to confirm MCP server can "
61
+ "reach the deployed App before invoking verdict fetches."
62
+ ),
63
+ inputSchema={
64
+ "type": "object",
65
+ "properties": {},
66
+ "required": [],
67
+ },
68
+ ),
69
+ ]
70
+
71
+
72
+ @server.call_tool()
73
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
74
+ if name == "cleanlib_fetch_verdict":
75
+ ecosystem = arguments.get("ecosystem", "")
76
+ package = arguments.get("package", "")
77
+ version = arguments.get("version", "")
78
+ if not (ecosystem and package and version):
79
+ return [TextContent(type="text", text="Error: ecosystem, package, and version are all required.")]
80
+ try:
81
+ verdict = await fetch_verdict(ecosystem, package, version)
82
+ except PermissionError as e:
83
+ return [TextContent(type="text", text=f"Auth error: {e}")]
84
+ except Exception as e:
85
+ return [TextContent(type="text", text=f"Verdict fetch failed: {e}")]
86
+ # Render as concise human-readable summary + JSON detail for agents.
87
+ icon = "✓" if verdict.decision == "ALLOW" else "✗" if verdict.decision == "DENY" else "⚠"
88
+ confidence_str = f"{verdict.confidence:.0%}" if verdict.confidence is not None else "n/a"
89
+ summary = (
90
+ f"{icon} {verdict.ecosystem}/{verdict.package}@{verdict.version} → "
91
+ f"{verdict.decision} (confidence {confidence_str})\n\n"
92
+ f"Reasoning: {verdict.reasoning or '(none)'}\n\n"
93
+ f"verdict_id: {verdict.verdict_id}"
94
+ )
95
+ return [TextContent(type="text", text=summary)]
96
+
97
+ if name == "cleanlib_health_check":
98
+ import os
99
+ endpoint = os.getenv("CLEANLIB_ENDPOINT", "").strip()
100
+ if not endpoint:
101
+ return [TextContent(type="text", text="CLEANLIB_ENDPOINT env var not set; cannot probe.")]
102
+ try:
103
+ data = await live_health(endpoint)
104
+ except Exception as e:
105
+ return [TextContent(type="text", text=f"Health probe failed against {endpoint}: {e}")]
106
+ return [TextContent(type="text", text=(
107
+ f"✓ CleanLibrary App reachable at {endpoint}\n\n"
108
+ f"status: {data.get('status')}\n"
109
+ f"service: {data.get('service')}\n"
110
+ f"version: {data.get('version')}\n"
111
+ f"ecosystems_mounted: {', '.join(data.get('ecosystems_mounted', []))}"
112
+ ))]
113
+
114
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
115
+
116
+
117
+ async def serve() -> None:
118
+ async with stdio_server() as (read_stream, write_stream):
119
+ await server.run(read_stream, write_stream, server.create_initialization_options())
120
+
121
+
122
+ def main() -> None:
123
+ logging.basicConfig(level=logging.INFO)
124
+ asyncio.run(serve())
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: cleanlib-mcp-server
3
+ Version: 0.2.0
4
+ Summary: CleanLibrary MCP server — exposes verdict-aware supply-chain risk assessment as Model Context Protocol tools for AI agent workflows
5
+ Author-email: CleanStart Inc <cto.office@cleanstart.com>
6
+ Maintainer-email: CleanStart Inc <cto.office@cleanstart.com>
7
+ License: CleanStart Inc Proprietary
8
+ Project-URL: Homepage, https://cleanlibrary.clnstrt.dev
9
+ Project-URL: Documentation, https://cleanlibrary.clnstrt.dev
10
+ Keywords: cleanlibrary,mcp,model-context-protocol,cleanstart,supply-chain-security,verdict,policy-evaluation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: License :: Other/Proprietary License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Software Distribution
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: mcp>=1.0.0
28
+ Requires-Dist: httpx>=0.27.0
29
+ Requires-Dist: pydantic>=2.7.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
32
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # cleanlib-mcp-server
37
+
38
+ CleanLibrary MCP (Model Context Protocol) server — expose verdict-aware supply-chain risk assessment as MCP tools, so MCP-capable clients (Claude Code, Claude Desktop, Cursor, GitHub Copilot, and other agents) can fetch package verdicts directly inside the developer's workflow.
39
+
40
+ Ask your AI assistant *"is cors@2.8.4 safe to install?"* and it queries CleanLibrary for an `ALLOW` / `DENY` / `WARN` verdict with reasoning and confidence — without leaving the editor.
41
+
42
+ ## Tools
43
+
44
+ | Tool | Description |
45
+ |---|---|
46
+ | `cleanlib_fetch_verdict(ecosystem, package, version)` | Fetch a verdict (`ALLOW` / `DENY` / `WARN`) with reasoning and confidence for a package version |
47
+ | `cleanlib_health_check()` | Report server status + whether a live CleanLibrary backend is configured |
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install cleanlib-mcp-server
53
+ ```
54
+
55
+ ## Run
56
+
57
+ ```bash
58
+ cleanlib-mcp-server # stdio transport (per MCP spec)
59
+ ```
60
+
61
+ ## Backend modes
62
+
63
+ - **Connected** — when `CLEANLIB_ENDPOINT` + `CLEANLIB_API_KEY` are set, the server queries your CleanLibrary deployment for live verdicts.
64
+ - **Local fixtures** — when no endpoint is configured (or the configured endpoint is unreachable), the server returns bundled demo fixtures so MCP clients always receive useful output.
65
+
66
+ ## MCP client integration
67
+
68
+ The server speaks standard MCP over stdio, so it works with any MCP-capable client. Example configuration (Claude Desktop — `~/Library/Application Support/Claude/claude_desktop_config.json`; other clients use the same `mcpServers` shape):
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "cleanlibrary": {
74
+ "command": "cleanlib-mcp-server"
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ To connect a live CleanLibrary backend, add the endpoint + API key:
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "cleanlibrary": {
86
+ "command": "cleanlib-mcp-server",
87
+ "env": {
88
+ "CLEANLIB_ENDPOINT": "https://cleanapp.clnstrt.dev",
89
+ "CLEANLIB_API_KEY": "clk_..."
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ The same `command` + `env` pattern applies to Cursor, GitHub Copilot, and other MCP clients — consult your client's MCP server configuration docs for the exact file location.
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ python -m venv .venv && source .venv/bin/activate
102
+ pip install -e ".[dev]"
103
+ ruff check src tests
104
+ pytest -v
105
+ ```
106
+
107
+ ## License
108
+
109
+ Proprietary. See [LICENSE](./LICENSE) for terms. © 2026 CleanStart Inc.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/cleanlib_mcp/__init__.py
5
+ src/cleanlib_mcp/backend.py
6
+ src/cleanlib_mcp/server.py
7
+ src/cleanlib_mcp_server.egg-info/PKG-INFO
8
+ src/cleanlib_mcp_server.egg-info/SOURCES.txt
9
+ src/cleanlib_mcp_server.egg-info/dependency_links.txt
10
+ src/cleanlib_mcp_server.egg-info/entry_points.txt
11
+ src/cleanlib_mcp_server.egg-info/requires.txt
12
+ src/cleanlib_mcp_server.egg-info/top_level.txt
13
+ tests/test_backend.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cleanlib-mcp-server = cleanlib_mcp.server:main
@@ -0,0 +1,8 @@
1
+ mcp>=1.0.0
2
+ httpx>=0.27.0
3
+ pydantic>=2.7.0
4
+
5
+ [dev]
6
+ pytest>=8.0.0
7
+ pytest-asyncio>=0.23.0
8
+ ruff>=0.6.0
@@ -0,0 +1,195 @@
1
+ """Substrate-mode backend tests — mock backend behavior with cycle-4 §D.7
2
+ demo fixtures."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+
8
+ import pytest
9
+
10
+ from cleanlib_mcp.backend import fetch_verdict
11
+
12
+
13
+ @pytest.fixture(autouse=True)
14
+ def _clear_live_env(monkeypatch):
15
+ """Force mock-mode by clearing live-backend env vars per test."""
16
+ monkeypatch.delenv("CLEANLIB_ENDPOINT", raising=False)
17
+ monkeypatch.delenv("CLEANLIB_API_KEY", raising=False)
18
+
19
+
20
+ async def test_cors_2_8_4_returns_deny():
21
+ v = await fetch_verdict("npm", "cors", "2.8.4")
22
+ assert v.decision == "DENY"
23
+ assert v.reasoning and ("prototype pollution" in v.reasoning or "2.8.5" in v.reasoning)
24
+
25
+
26
+ async def test_cors_2_8_5_returns_allow():
27
+ v = await fetch_verdict("npm", "cors", "2.8.5")
28
+ assert v.decision == "ALLOW"
29
+
30
+
31
+ async def test_helmet_returns_allow():
32
+ v = await fetch_verdict("npm", "helmet", "8.0.0")
33
+ assert v.decision == "ALLOW"
34
+
35
+
36
+ async def test_dotenv_returns_allow():
37
+ v = await fetch_verdict("npm", "dotenv", "16.4.5")
38
+ assert v.decision == "ALLOW"
39
+
40
+
41
+ async def test_oracledb_returns_warn():
42
+ v = await fetch_verdict("npm", "oracledb", "6.5.1")
43
+ assert v.decision == "WARN"
44
+ assert v.reasoning and ("vendor-binary" in v.reasoning.lower() or "supply-chain" in v.reasoning.lower())
45
+
46
+
47
+ async def test_unknown_pkg_default_allow():
48
+ v = await fetch_verdict("npm", "some-unknown-pkg", "1.0.0")
49
+ assert v.decision == "ALLOW"
50
+ assert v.reasoning and "default-allow" in v.reasoning.lower()
51
+
52
+
53
+ async def test_verdict_id_unique_per_call():
54
+ a = await fetch_verdict("npm", "lodash", "4.17.21")
55
+ b = await fetch_verdict("npm", "lodash", "4.17.21")
56
+ assert a.verdict_id != b.verdict_id
57
+
58
+
59
+ async def test_verdict_carries_input_eco_pkg_ver():
60
+ v = await fetch_verdict("pypi", "requests", "2.32.0")
61
+ assert v.ecosystem == "pypi"
62
+ assert v.package == "requests"
63
+ assert v.version == "2.32.0"
64
+
65
+
66
+ async def test_verdict_to_dict_drops_none_values():
67
+ v = await fetch_verdict("npm", "helmet", "8.0.0")
68
+ d = v.to_dict()
69
+ # None values should be dropped from serialization
70
+ assert all(value is not None for value in d.values())
71
+ assert d["decision"] == "ALLOW"
72
+ assert d["package"] == "helmet"
73
+
74
+
75
+ def _mock_transport(handler):
76
+ """Helper: build an httpx.MockTransport from a request-handler callable."""
77
+ import httpx
78
+ return httpx.MockTransport(handler)
79
+
80
+
81
+ async def test_live_fetch_200_returns_canonical_verdict(monkeypatch):
82
+ """When App's Rev 4 §9.3 endpoint returns 200 + valid JSON, MCP parses
83
+ it into the canonical Verdict dataclass."""
84
+ import httpx
85
+ from cleanlib_mcp import backend as b
86
+
87
+ def handler(request):
88
+ assert request.url.path == "/v1/customer/verdicts/npm/lodash/4.17.21"
89
+ assert request.headers["authorization"] == "Bearer test-key"
90
+ return httpx.Response(200, json={
91
+ "verdict_id": "vrd_test123",
92
+ "ecosystem": "npm",
93
+ "package": "lodash",
94
+ "version": "4.17.21",
95
+ "decision": "ALLOW",
96
+ "reasoning": "no findings",
97
+ "confidence": 0.95,
98
+ "source": "ALLOWED_NO_FINDINGS",
99
+ })
100
+
101
+ _orig = httpx.AsyncClient
102
+ monkeypatch.setattr(b.httpx, "AsyncClient", lambda **kw: _orig(transport=_mock_transport(handler), **{k: v for k, v in kw.items() if k != "transport"}))
103
+ v = await b._live_fetch("https://app.test", "test-key", "npm", "lodash", "4.17.21")
104
+ assert v.verdict_id == "vrd_test123"
105
+ assert v.decision == "ALLOW"
106
+ assert v.confidence == 0.95
107
+
108
+
109
+ async def test_live_fetch_404_no_json_raises_live_backend_not_deployed(monkeypatch):
110
+ """When App's endpoint returns 404 with empty/non-JSON body, MCP raises
111
+ LiveBackendNotDeployed so the wrapper can fall back to mock."""
112
+ import httpx
113
+ from cleanlib_mcp import backend as b
114
+
115
+ def handler(request):
116
+ return httpx.Response(404, content=b"<html>404 Not Found</html>", headers={"content-type": "text/html"})
117
+
118
+ _orig = httpx.AsyncClient
119
+ monkeypatch.setattr(b.httpx, "AsyncClient", lambda **kw: _orig(transport=_mock_transport(handler), **{k: v for k, v in kw.items() if k != "transport"}))
120
+ with pytest.raises(b.LiveBackendNotDeployed) as exc:
121
+ await b._live_fetch("https://app.test", "test-key", "npm", "lodash", "4.17.21")
122
+ assert "did not return a verdict" in str(exc.value)
123
+
124
+
125
+ async def test_live_fetch_404_json_returns_no_verdict_substrate(monkeypatch):
126
+ """When the endpoint returns 404 with a JSON body (reached, but no verdict
127
+ for this package), the backend returns a Verdict with INSUFFICIENT_DATA
128
+ source."""
129
+ import httpx
130
+ from cleanlib_mcp import backend as b
131
+
132
+ def handler(request):
133
+ return httpx.Response(404, json={"error": "verdict not found"}, headers={"content-type": "application/json"})
134
+
135
+ _orig = httpx.AsyncClient
136
+ monkeypatch.setattr(b.httpx, "AsyncClient", lambda **kw: _orig(transport=_mock_transport(handler), **{k: v for k, v in kw.items() if k != "transport"}))
137
+ v = await b._live_fetch("https://app.test", "test-key", "npm", "missing", "1.0.0")
138
+ assert v.decision == "ALLOW"
139
+ assert v.source == "INSUFFICIENT_DATA"
140
+ assert "No verdict on file" in (v.reasoning or "")
141
+
142
+
143
+ async def test_live_fetch_401_raises_permission_error(monkeypatch):
144
+ """Bad bearer → 401 → PermissionError."""
145
+ import httpx
146
+ from cleanlib_mcp import backend as b
147
+
148
+ def handler(request):
149
+ return httpx.Response(401, content=b"unauthorized")
150
+
151
+ _orig = httpx.AsyncClient
152
+ monkeypatch.setattr(b.httpx, "AsyncClient", lambda **kw: _orig(transport=_mock_transport(handler), **{k: v for k, v in kw.items() if k != "transport"}))
153
+ with pytest.raises(PermissionError) as exc:
154
+ await b._live_fetch("https://app.test", "bad-key", "npm", "lodash", "4.17.21")
155
+ assert "401" in str(exc.value)
156
+
157
+
158
+ async def test_fetch_verdict_falls_back_to_mock_when_endpoint_not_deployed(monkeypatch):
159
+ """End-to-end: live env set, App returns 404-not-deployed, fetch_verdict
160
+ falls back to mock fixture + annotates reasoning."""
161
+ import httpx
162
+ from cleanlib_mcp import backend as b
163
+
164
+ monkeypatch.setenv("CLEANLIB_ENDPOINT", "https://app.test")
165
+ monkeypatch.setenv("CLEANLIB_API_KEY", "test-key")
166
+
167
+ def handler(request):
168
+ return httpx.Response(404, content=b"<html>404</html>", headers={"content-type": "text/html"})
169
+
170
+ _orig = httpx.AsyncClient
171
+ monkeypatch.setattr(b.httpx, "AsyncClient", lambda **kw: _orig(transport=_mock_transport(handler), **{k: v for k, v in kw.items() if k != "transport"}))
172
+ v = await b.fetch_verdict("npm", "cors", "2.8.4")
173
+ assert v.decision == "DENY" # cors-2.8.4 fixture is DENY
174
+ assert "live-mode fallback" in (v.reasoning or "")
175
+
176
+
177
+ async def test_live_health_returns_app_health_json(monkeypatch):
178
+ """live_health probes /health and returns the JSON body."""
179
+ import httpx
180
+ from cleanlib_mcp import backend as b
181
+
182
+ def handler(request):
183
+ assert request.url.path == "/health"
184
+ return httpx.Response(200, json={
185
+ "status": "ok",
186
+ "service": "cleanlib-app",
187
+ "version": "0.1.0",
188
+ "ecosystems_mounted": ["npm", "pypi", "go", "maven", "crates", "nuget", "rubygems"],
189
+ })
190
+
191
+ _orig = httpx.AsyncClient
192
+ monkeypatch.setattr(b.httpx, "AsyncClient", lambda **kw: _orig(transport=_mock_transport(handler), **{k: v for k, v in kw.items() if k != "transport"}))
193
+ data = await b.live_health("https://app.test")
194
+ assert data["status"] == "ok"
195
+ assert "npm" in data["ecosystems_mounted"]