sciple-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,3 @@
1
+ SCIPLE_API_URL=http://localhost:8000/api/v1
2
+ SCIPLE_API_TOKEN=sciple_pat_your_personal_access_token_here
3
+ SCIPLE_TENANT_ID=your-tenant-id-here
@@ -0,0 +1,50 @@
1
+ name: Publish to PyPI
2
+
3
+ # Tag-driven release: bump `version` in pyproject.toml, commit, then
4
+ # git tag v0.1.0 && git push origin v0.1.0
5
+ # Publishing pauses for manual approval (the `pypi` environment) before
6
+ # the upload step runs.
7
+
8
+ on:
9
+ push:
10
+ tags:
11
+ - "v*"
12
+
13
+ jobs:
14
+ build:
15
+ name: Build sdist + wheel
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v3
22
+
23
+ - name: Build distributions
24
+ run: uv build
25
+
26
+ - name: Upload built distributions
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish:
33
+ name: Publish to PyPI via OIDC
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: pypi
38
+ url: https://pypi.org/project/sciple-mcp/
39
+ permissions:
40
+ # Required for PyPI Trusted Publishing (OIDC). No PyPI API token needed.
41
+ id-token: write
42
+ steps:
43
+ - name: Download built distributions
44
+ uses: actions/download-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+
49
+ - name: Publish to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ *.egg
7
+ build/
8
+ dist/
9
+ .eggs/
10
+
11
+ # uv / virtualenvs
12
+ .venv/
13
+ .python-version
14
+
15
+ # Test / caches
16
+ .pytest_cache/
17
+ .coverage
18
+ .coverage.*
19
+ htmlcov/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+
23
+ # Env files (secrets — never commit)
24
+ .env
25
+ .env.local
26
+
27
+ # Editors
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+
32
+ # OS
33
+ .DS_Store
34
+ Thumbs.db
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: sciple-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for populating and managing Sciple platform content
5
+ Project-URL: Homepage, https://github.com/navaganeshr/sciple-mcp
6
+ Project-URL: Source, https://github.com/navaganeshr/sciple-mcp
7
+ Project-URL: Issues, https://github.com/navaganeshr/sciple-mcp/issues
8
+ Project-URL: Documentation, https://github.com/navaganeshr/sciple-mcp#readme
9
+ Author-email: Navaganesh Raju <navaganesh.raju@gmail.com>
10
+ Keywords: anthropic,claude,mcp,model-context-protocol,sciple
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: mcp[cli]>=1.0.0
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Sciple Platform MCP Server
25
+
26
+ MCP server that lets a local Claude populate and manage Sciple platform content — environments, environment groups, and services — via the Sciple REST API. Engineers use it to bootstrap tenant structure and maintain the service catalog without leaving their AI coding session.
27
+
28
+ ## Setup
29
+
30
+ ```bash
31
+ cd services/sciple-mcp
32
+ uv sync
33
+ ```
34
+
35
+ **Environment variables** (`.env` or shell):
36
+
37
+ ```
38
+ SCIPLE_API_URL=http://localhost:8000/api/v1
39
+ SCIPLE_API_TOKEN=sciple_pat_...
40
+ SCIPLE_TENANT_ID=<your tenant id>
41
+ ```
42
+
43
+ `SCIPLE_API_TOKEN` is a personal access token minted under **Profile → Access tokens** in the dashboard, scoped to the permissions the server should have (e.g. `environments.view`, `environments.manage`, `services.view`, `services.manage`, `observability.view`, `observability.manage`). The PAT is single-tenant — its bound tenant must equal `SCIPLE_TENANT_ID`.
44
+
45
+ ## Wire into Claude Desktop / Claude Code
46
+
47
+ Add to `~/.claude/claude_desktop_config.json` (or your Claude Code MCP config):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "sciple-platform": {
53
+ "command": "uv",
54
+ "args": ["run", "--project", "/path/to/services/sciple-mcp", "sciple-mcp"],
55
+ "env": {
56
+ "SCIPLE_API_URL": "http://localhost:8000/api/v1",
57
+ "SCIPLE_API_TOKEN": "sciple_pat_...",
58
+ "SCIPLE_TENANT_ID": "..."
59
+ }
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ Then restart Claude Desktop or Claude Code. You should see 17 platform tools available.
66
+
67
+ ## Tools
68
+
69
+ ### Environments
70
+
71
+ | Tool | Description |
72
+ |---|---|
73
+ | `list_environments` | List all environments in the tenant (id, name, slug, group, default flag) |
74
+ | `create_environment` | Create an environment with optional group assignment and default flag |
75
+ | `update_environment` | Update an environment's name, description, group, or sort order |
76
+ | `delete_environment` | Delete an environment by id (irreversible) |
77
+ | `list_environment_groups` | List environment groups (id, name, slug, AWS account binding) |
78
+ | `create_environment_group` | Create an environment group with optional AWS account binding |
79
+
80
+ ### Observability
81
+
82
+ | Tool | Description |
83
+ |---|---|
84
+ | `list_dashboards` | List all observability dashboards in the tenant (id, name, panel count) |
85
+ | `get_dashboard` | Get a dashboard's name, description, and panel list |
86
+ | `create_dashboard` | Create a new dashboard with optional description |
87
+ | `update_dashboard` | Replace a dashboard's name and description (full PUT; name required) |
88
+ | `delete_dashboard` | Delete a dashboard and all its panels (irreversible) |
89
+ | `add_panel` | Add a panel to a dashboard with optional PromQL query |
90
+ | `delete_panel` | Delete a panel from a dashboard (irreversible) |
91
+
92
+ ### Services
93
+
94
+ | Tool | Description |
95
+ |---|---|
96
+ | `list_services` | List all services in the tenant catalog (id, name, slug) |
97
+ | `create_service` | Create a service in the catalog with kind, language, SCM provider, and repository |
98
+ | `update_service` | Update a service's metadata, lifecycle, owner, tags, links, or environment associations |
99
+ | `delete_service` | Delete a service from the catalog by id (irreversible) |
100
+
101
+ ## Security
102
+
103
+ The server can only do what the PAT's scope allows. Attempts to write without the relevant manage permission return a 403 from the API and are surfaced as an error in Claude's response. The PAT is revocable at any time from **Profile → Access tokens** in the Sciple dashboard — revoking it immediately cuts off the server's access without any config change.
@@ -0,0 +1,80 @@
1
+ # Sciple Platform MCP Server
2
+
3
+ MCP server that lets a local Claude populate and manage Sciple platform content — environments, environment groups, and services — via the Sciple REST API. Engineers use it to bootstrap tenant structure and maintain the service catalog without leaving their AI coding session.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ cd services/sciple-mcp
9
+ uv sync
10
+ ```
11
+
12
+ **Environment variables** (`.env` or shell):
13
+
14
+ ```
15
+ SCIPLE_API_URL=http://localhost:8000/api/v1
16
+ SCIPLE_API_TOKEN=sciple_pat_...
17
+ SCIPLE_TENANT_ID=<your tenant id>
18
+ ```
19
+
20
+ `SCIPLE_API_TOKEN` is a personal access token minted under **Profile → Access tokens** in the dashboard, scoped to the permissions the server should have (e.g. `environments.view`, `environments.manage`, `services.view`, `services.manage`, `observability.view`, `observability.manage`). The PAT is single-tenant — its bound tenant must equal `SCIPLE_TENANT_ID`.
21
+
22
+ ## Wire into Claude Desktop / Claude Code
23
+
24
+ Add to `~/.claude/claude_desktop_config.json` (or your Claude Code MCP config):
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "sciple-platform": {
30
+ "command": "uv",
31
+ "args": ["run", "--project", "/path/to/services/sciple-mcp", "sciple-mcp"],
32
+ "env": {
33
+ "SCIPLE_API_URL": "http://localhost:8000/api/v1",
34
+ "SCIPLE_API_TOKEN": "sciple_pat_...",
35
+ "SCIPLE_TENANT_ID": "..."
36
+ }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ Then restart Claude Desktop or Claude Code. You should see 17 platform tools available.
43
+
44
+ ## Tools
45
+
46
+ ### Environments
47
+
48
+ | Tool | Description |
49
+ |---|---|
50
+ | `list_environments` | List all environments in the tenant (id, name, slug, group, default flag) |
51
+ | `create_environment` | Create an environment with optional group assignment and default flag |
52
+ | `update_environment` | Update an environment's name, description, group, or sort order |
53
+ | `delete_environment` | Delete an environment by id (irreversible) |
54
+ | `list_environment_groups` | List environment groups (id, name, slug, AWS account binding) |
55
+ | `create_environment_group` | Create an environment group with optional AWS account binding |
56
+
57
+ ### Observability
58
+
59
+ | Tool | Description |
60
+ |---|---|
61
+ | `list_dashboards` | List all observability dashboards in the tenant (id, name, panel count) |
62
+ | `get_dashboard` | Get a dashboard's name, description, and panel list |
63
+ | `create_dashboard` | Create a new dashboard with optional description |
64
+ | `update_dashboard` | Replace a dashboard's name and description (full PUT; name required) |
65
+ | `delete_dashboard` | Delete a dashboard and all its panels (irreversible) |
66
+ | `add_panel` | Add a panel to a dashboard with optional PromQL query |
67
+ | `delete_panel` | Delete a panel from a dashboard (irreversible) |
68
+
69
+ ### Services
70
+
71
+ | Tool | Description |
72
+ |---|---|
73
+ | `list_services` | List all services in the tenant catalog (id, name, slug) |
74
+ | `create_service` | Create a service in the catalog with kind, language, SCM provider, and repository |
75
+ | `update_service` | Update a service's metadata, lifecycle, owner, tags, links, or environment associations |
76
+ | `delete_service` | Delete a service from the catalog by id (irreversible) |
77
+
78
+ ## Security
79
+
80
+ The server can only do what the PAT's scope allows. Attempts to write without the relevant manage permission return a 403 from the API and are surfaced as an error in Claude's response. The PAT is revocable at any time from **Profile → Access tokens** in the Sciple dashboard — revoking it immediately cuts off the server's access without any config change.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sciple-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for populating and managing Sciple platform content"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ authors = [
12
+ { name = "Navaganesh Raju", email = "navaganesh.raju@gmail.com" },
13
+ ]
14
+ keywords = ["mcp", "sciple", "claude", "anthropic", "model-context-protocol"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: System Administrators",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries",
22
+ "Topic :: System :: Systems Administration",
23
+ ]
24
+ dependencies = [
25
+ "mcp[cli]>=1.0.0",
26
+ "httpx>=0.27.0",
27
+ "python-dotenv>=1.0.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/navaganeshr/sciple-mcp"
32
+ Source = "https://github.com/navaganeshr/sciple-mcp"
33
+ Issues = "https://github.com/navaganeshr/sciple-mcp/issues"
34
+ Documentation = "https://github.com/navaganeshr/sciple-mcp#readme"
35
+
36
+ [project.scripts]
37
+ sciple-mcp = "sciple_mcp.server:main"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/sciple_mcp"]
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=8.0.0",
45
+ "pytest-asyncio>=0.23.0",
46
+ "respx>=0.21.0",
47
+ ]
48
+
49
+ [tool.pytest.ini_options]
50
+ asyncio_mode = "auto"
File without changes
@@ -0,0 +1,46 @@
1
+ """Thin async HTTP client wrapping the Sciple REST API.
2
+
3
+ Authenticates with a Sciple personal access token (PAT). The PAT is sent as a
4
+ Bearer token; the API's get_bearer resolves it on data routes, and X-Tenant-ID
5
+ selects the tenant (which must match the PAT's bound tenant).
6
+ """
7
+ import httpx
8
+
9
+
10
+ class ScipleClient:
11
+ def __init__(self, base_url: str, token: str, tenant_id: str) -> None:
12
+ self._base = base_url.rstrip("/")
13
+ self._headers = {
14
+ "Authorization": f"Bearer {token}",
15
+ "X-Tenant-ID": tenant_id,
16
+ "Content-Type": "application/json",
17
+ }
18
+
19
+ async def get(self, path: str) -> object:
20
+ async with httpx.AsyncClient() as http:
21
+ r = await http.get(f"{self._base}{path}", headers=self._headers)
22
+ r.raise_for_status()
23
+ return r.json()
24
+
25
+ async def post(self, path: str, body: dict | None = None) -> object:
26
+ async with httpx.AsyncClient() as http:
27
+ r = await http.post(f"{self._base}{path}", headers=self._headers, json=body or {})
28
+ r.raise_for_status()
29
+ return r.json()
30
+
31
+ async def patch(self, path: str, body: dict) -> object:
32
+ async with httpx.AsyncClient() as http:
33
+ r = await http.patch(f"{self._base}{path}", headers=self._headers, json=body)
34
+ r.raise_for_status()
35
+ return r.json()
36
+
37
+ async def put(self, path: str, body: dict) -> object:
38
+ async with httpx.AsyncClient() as http:
39
+ r = await http.put(f"{self._base}{path}", headers=self._headers, json=body)
40
+ r.raise_for_status()
41
+ return r.json()
42
+
43
+ async def delete(self, path: str) -> None:
44
+ async with httpx.AsyncClient() as http:
45
+ r = await http.delete(f"{self._base}{path}", headers=self._headers)
46
+ r.raise_for_status()
@@ -0,0 +1,42 @@
1
+ """Sciple platform MCP server — exposes platform CRUD tools to a local Claude.
2
+
3
+ Auth: a Sciple personal access token (PAT) in SCIPLE_API_TOKEN. The server's
4
+ reach is whatever the PAT's scope grants; out-of-scope writes fail server-side.
5
+ """
6
+ import os
7
+
8
+ from dotenv import load_dotenv
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from sciple_mcp.client import ScipleClient
12
+ from sciple_mcp.tools import environments, observability, services
13
+
14
+ load_dotenv()
15
+
16
+ mcp = FastMCP("Sciple Platform")
17
+
18
+ _client: ScipleClient | None = None
19
+
20
+
21
+ def _get_client() -> ScipleClient:
22
+ global _client
23
+ if _client is None:
24
+ _client = ScipleClient(
25
+ base_url=os.environ["SCIPLE_API_URL"],
26
+ token=os.environ["SCIPLE_API_TOKEN"],
27
+ tenant_id=os.environ["SCIPLE_TENANT_ID"],
28
+ )
29
+ return _client
30
+
31
+
32
+ environments.register(mcp, _get_client)
33
+ observability.register(mcp, _get_client)
34
+ services.register(mcp, _get_client)
35
+
36
+
37
+ def main() -> None:
38
+ mcp.run(transport="stdio")
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
File without changes
@@ -0,0 +1,138 @@
1
+ """Environment + environment-group tools — populate the platform's env structure.
2
+
3
+ Wraps the REST routes:
4
+ GET/POST /environments GET/PATCH/DELETE /environments/{id}
5
+ GET/POST /envgroups GET/PATCH/DELETE /envgroups/{id}
6
+ Requires the PAT to hold environments.view (reads) / environments.manage (writes).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+
12
+ from sciple_mcp.client import ScipleClient
13
+
14
+
15
+ def register(mcp, get_client: Callable[[], ScipleClient]) -> None:
16
+
17
+ @mcp.tool()
18
+ async def list_environments() -> str:
19
+ """List all environments in the tenant (id, name, slug, group, default flag)."""
20
+ envs = await get_client().get("/environments")
21
+ if not envs:
22
+ return "No environments found."
23
+ lines = []
24
+ for e in envs:
25
+ default = " [default]" if e.get("is_default") else ""
26
+ grp = f" group={e['environment_group_id']}" if e.get("environment_group_id") else ""
27
+ lines.append(f"- {e['name']} (id={e['id']}, slug={e['slug']}){default}{grp}")
28
+ return "\n".join(lines)
29
+
30
+ @mcp.tool()
31
+ async def create_environment(
32
+ name: str,
33
+ slug: str | None = None,
34
+ environment_group_id: str | None = None,
35
+ description: str | None = None,
36
+ is_default: bool = False,
37
+ display_order: int = 0,
38
+ ) -> str:
39
+ """Create an environment.
40
+
41
+ Args:
42
+ name: Display name, e.g. "Production".
43
+ slug: URL-safe id; server generates one from name if omitted.
44
+ environment_group_id: Optional parent environment-group id.
45
+ description: Optional human description.
46
+ is_default: Mark this the tenant's default environment.
47
+ display_order: Sort order in the UI (lower = earlier).
48
+ """
49
+ body: dict = {"name": name, "is_default": is_default, "display_order": display_order}
50
+ if slug is not None:
51
+ body["slug"] = slug
52
+ if environment_group_id is not None:
53
+ body["environment_group_id"] = environment_group_id
54
+ if description is not None:
55
+ body["description"] = description
56
+ e = await get_client().post("/environments", body)
57
+ return f"Created environment '{e['name']}' (id={e['id']}, slug={e['slug']})."
58
+
59
+ @mcp.tool()
60
+ async def update_environment(
61
+ env_id: str,
62
+ name: str | None = None,
63
+ description: str | None = None,
64
+ environment_group_id: str | None = None,
65
+ is_default: bool | None = None,
66
+ display_order: int | None = None,
67
+ ) -> str:
68
+ """Update an environment. Only provided fields change.
69
+
70
+ Args:
71
+ env_id: The environment id to update.
72
+ name: New display name.
73
+ description: New description.
74
+ environment_group_id: Move to a different environment group.
75
+ is_default: Set/unset default.
76
+ display_order: New sort order.
77
+ """
78
+ body: dict = {}
79
+ for key, val in (
80
+ ("name", name), ("description", description),
81
+ ("environment_group_id", environment_group_id),
82
+ ("is_default", is_default), ("display_order", display_order),
83
+ ):
84
+ if val is not None:
85
+ body[key] = val
86
+ if not body:
87
+ return "Nothing to update — provide at least one field."
88
+ e = await get_client().patch(f"/environments/{env_id}", body)
89
+ return f"Updated environment '{e['name']}' (id={e['id']})."
90
+
91
+ @mcp.tool()
92
+ async def delete_environment(env_id: str) -> str:
93
+ """Delete an environment by id. This cannot be undone.
94
+
95
+ Args:
96
+ env_id: The environment id to delete.
97
+ """
98
+ await get_client().delete(f"/environments/{env_id}")
99
+ return f"Deleted environment {env_id}."
100
+
101
+ @mcp.tool()
102
+ async def list_environment_groups() -> str:
103
+ """List environment groups (id, name, slug, aws account binding)."""
104
+ groups = await get_client().get("/envgroups")
105
+ if not groups:
106
+ return "No environment groups found."
107
+ lines = []
108
+ for g in groups:
109
+ acct = f" aws={g['aws_account_id']}" if g.get("aws_account_id") else ""
110
+ lines.append(f"- {g['name']} (id={g['id']}, slug={g['slug']}){acct}")
111
+ return "\n".join(lines)
112
+
113
+ @mcp.tool()
114
+ async def create_environment_group(
115
+ name: str,
116
+ slug: str | None = None,
117
+ description: str | None = None,
118
+ display_order: int = 0,
119
+ aws_account_id: str | None = None,
120
+ ) -> str:
121
+ """Create an environment group.
122
+
123
+ Args:
124
+ name: Display name, e.g. "Production accounts".
125
+ slug: URL-safe id; generated from name if omitted.
126
+ description: Optional description.
127
+ display_order: Sort order in the UI.
128
+ aws_account_id: Optional AWS account id to bind the group to.
129
+ """
130
+ body: dict = {"name": name, "display_order": display_order}
131
+ if slug is not None:
132
+ body["slug"] = slug
133
+ if description is not None:
134
+ body["description"] = description
135
+ if aws_account_id is not None:
136
+ body["aws_account_id"] = aws_account_id
137
+ g = await get_client().post("/envgroups", body)
138
+ return f"Created environment group '{g['name']}' (id={g['id']}, slug={g['slug']})."