mcp-devops-assistant 0.1.0__py3-none-any.whl

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,102 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ from fastmcp import FastMCP
5
+
6
+ from ..audit import audit_tool
7
+ from ..errors import InvalidInputError
8
+ from ..security import limit
9
+ from ..services.github_client import GitHubClient
10
+
11
+ _MAX_RESULTS = 100
12
+
13
+
14
+ def _validate_limit(count: int) -> int:
15
+ if not 1 <= count <= _MAX_RESULTS:
16
+ raise InvalidInputError(f"limit must be between 1 and {_MAX_RESULTS}")
17
+ return count
18
+
19
+
20
+ def _validate_state(state: str) -> str:
21
+ if state not in {"open", "closed", "all"}:
22
+ raise InvalidInputError("state must be 'open', 'closed', or 'all'")
23
+ return state
24
+
25
+
26
+ def _json_result(data: Any, max_chars: int) -> str:
27
+ return limit(json.dumps(data, ensure_ascii=True, separators=(",", ":")), max_chars)
28
+
29
+
30
+ def register(mcp: FastMCP, client: GitHubClient) -> None:
31
+ @mcp.tool
32
+ @audit_tool
33
+ async def github_list_repositories(limit: int = 30) -> str:
34
+ """List repositories accessible to the configured GitHub token, subject to the repository allowlist."""
35
+ count = _validate_limit(limit)
36
+ return _json_result(
37
+ await client.list_repositories(count), client.settings.max_output_chars
38
+ )
39
+
40
+ @mcp.tool
41
+ @audit_tool
42
+ async def github_get_repository(repository: str) -> str:
43
+ """Return safe metadata for an accessible owner/repository."""
44
+ return _json_result(
45
+ await client.get_repository(repository), client.settings.max_output_chars
46
+ )
47
+
48
+ @mcp.tool
49
+ @audit_tool
50
+ async def github_list_files(
51
+ repository: str, path: str = "", ref: str | None = None, limit: int = 100
52
+ ) -> str:
53
+ """List files and directories at a repository path and ref, with a bounded result count."""
54
+ count = _validate_limit(limit)
55
+ return _json_result(
56
+ await client.list_files(repository, path, ref, count),
57
+ client.settings.max_output_chars,
58
+ )
59
+
60
+ @mcp.tool
61
+ @audit_tool
62
+ async def github_read_file(
63
+ repository: str, path: str, ref: str | None = None
64
+ ) -> str:
65
+ """Read a bounded, redacted UTF-8 text file from an accessible repository."""
66
+ return await client.read_file(repository, path, ref)
67
+
68
+ @mcp.tool
69
+ @audit_tool
70
+ async def github_list_commits(
71
+ repository: str, branch: str | None = None, limit: int = 30
72
+ ) -> str:
73
+ """List recent commits for a repository and optional branch."""
74
+ count = _validate_limit(limit)
75
+ return _json_result(
76
+ await client.list_commits(repository, branch, count),
77
+ client.settings.max_output_chars,
78
+ )
79
+
80
+ @mcp.tool
81
+ @audit_tool
82
+ async def github_list_issues(
83
+ repository: str, state: str = "open", limit: int = 30
84
+ ) -> str:
85
+ """List issues (excluding pull requests) for a repository."""
86
+ count = _validate_limit(limit)
87
+ return _json_result(
88
+ await client.list_issues(repository, _validate_state(state), count),
89
+ client.settings.max_output_chars,
90
+ )
91
+
92
+ @mcp.tool
93
+ @audit_tool
94
+ async def github_list_pull_requests(
95
+ repository: str, state: str = "open", limit: int = 30
96
+ ) -> str:
97
+ """List pull requests for a repository."""
98
+ count = _validate_limit(limit)
99
+ return _json_result(
100
+ await client.list_pull_requests(repository, _validate_state(state), count),
101
+ client.settings.max_output_chars,
102
+ )
@@ -0,0 +1,161 @@
1
+ import asyncio
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ from ..actions import action_manager
6
+ from ..audit import audit_tool
7
+ from ..config import Settings
8
+ from ..errors import InvalidInputError, PolicyDeniedError
9
+ from ..security import redact
10
+ from ..services.docker_client import DockerClient
11
+ from ..services.github_client import GitHubClient
12
+
13
+
14
+ def register(
15
+ mcp: FastMCP,
16
+ settings: Settings,
17
+ github: GitHubClient,
18
+ docker: DockerClient,
19
+ ) -> None:
20
+ if not settings.enable_write_tools:
21
+ return
22
+
23
+ @mcp.tool
24
+ @audit_tool
25
+ async def github_create_branch(
26
+ repository: str, branch: str, base_branch: str
27
+ ) -> str:
28
+ """Stage creation of an allowlisted GitHub branch; confirmation is required to create it."""
29
+ github.require_write_token()
30
+ details = await github.prepare_branch_creation(repository, branch, base_branch)
31
+ description = (
32
+ f"Create branch {details['branch']} in {details['repository']} "
33
+ f"from {details['base_branch']} ({details['base_sha']})"
34
+ )
35
+
36
+ async def execute() -> dict:
37
+ return await github.create_branch(repository, branch, details["base_sha"])
38
+
39
+ return action_manager.stage(
40
+ "github_create_branch",
41
+ f"{repository}:{branch}",
42
+ description,
43
+ {
44
+ "repository": repository,
45
+ "branch": branch,
46
+ "base_branch": base_branch,
47
+ "base_commit": details["base_sha"],
48
+ },
49
+ execute,
50
+ settings.max_output_chars,
51
+ )
52
+
53
+ @mcp.tool
54
+ @audit_tool
55
+ async def github_commit_file(
56
+ repository: str,
57
+ branch: str,
58
+ path: str,
59
+ content: str,
60
+ message: str,
61
+ ) -> str:
62
+ """Stage one file create/update on an allowlisted prefixed branch; exact content is shown for confirmation."""
63
+ github.require_write_token()
64
+ if redact(message) != message:
65
+ raise PolicyDeniedError("Commit message appears to contain a secret")
66
+ details = await github.prepare_file_update(
67
+ repository, path, branch, content, message
68
+ )
69
+ operation = "Update" if details["sha"] else "Create"
70
+ description = (
71
+ f"{operation} {details['path']} in {repository} on branch {branch}"
72
+ )
73
+
74
+ async def execute() -> dict:
75
+ return await github.commit_file(
76
+ repository,
77
+ details["path"],
78
+ branch,
79
+ content,
80
+ message,
81
+ details["sha"],
82
+ )
83
+
84
+ return action_manager.stage(
85
+ "github_commit_file",
86
+ f"{repository}:{branch}:{path}",
87
+ description,
88
+ {
89
+ "repository": repository,
90
+ "branch": branch,
91
+ "path": details["path"],
92
+ "commit_message": message,
93
+ "existing_file": details["sha"] is not None,
94
+ "proposed_content": content,
95
+ },
96
+ execute,
97
+ settings.max_output_chars,
98
+ )
99
+
100
+ @mcp.tool
101
+ @audit_tool
102
+ async def github_create_pull_request(
103
+ repository: str,
104
+ head: str,
105
+ base: str,
106
+ title: str,
107
+ body: str = "",
108
+ ) -> str:
109
+ """Stage a pull request from an allowlisted prefixed branch for human review; no merge operation is available."""
110
+ github.require_write_token()
111
+ if not title.strip() or len(title) > 256:
112
+ raise InvalidInputError(
113
+ "Pull request title must contain 1 to 256 characters"
114
+ )
115
+ if len(body) > settings.max_output_chars // 2:
116
+ raise InvalidInputError(
117
+ "Pull request body is too large to show in the confirmation response"
118
+ )
119
+ if redact(title) != title or redact(body) != body:
120
+ raise PolicyDeniedError("Pull request details appear to contain a secret")
121
+ await github.validate_write_branch(repository, head)
122
+ await github.get_branch_sha(repository, base)
123
+ description = f"Open pull request in {repository}: {head} -> {base} ({title})"
124
+
125
+ async def execute() -> dict:
126
+ return await github.create_pull_request(repository, head, base, title, body)
127
+
128
+ return action_manager.stage(
129
+ "github_create_pull_request",
130
+ f"{repository}:{head}->{base}",
131
+ description,
132
+ {
133
+ "repository": repository,
134
+ "head": head,
135
+ "base": base,
136
+ "title": title,
137
+ "body": body,
138
+ },
139
+ execute,
140
+ settings.max_output_chars,
141
+ )
142
+
143
+ @mcp.tool
144
+ @audit_tool
145
+ async def docker_restart_container(name: str) -> str:
146
+ """Stage restart of an explicitly allowlisted non-production container; confirmation is required."""
147
+ details = await asyncio.to_thread(docker.prepare_container_restart, name)
148
+ description = f"Restart allowlisted development container {details['name']}"
149
+ return action_manager.stage(
150
+ "docker_restart_container",
151
+ details["name"],
152
+ description,
153
+ details,
154
+ lambda: docker.restart_container(name),
155
+ settings.max_output_chars,
156
+ )
157
+
158
+ @mcp.tool
159
+ async def confirm_action(token: str) -> str:
160
+ """Perform exactly one staged action if its confirmation token is valid and unexpired."""
161
+ return await action_manager.confirm(token, settings.enable_write_tools)
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-devops-assistant
3
+ Version: 0.1.0
4
+ Summary: DevOps Assistant MCP Server for GitHub and Docker
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastmcp
8
+ Requires-Dist: httpx
9
+ Requires-Dist: docker
10
+ Requires-Dist: python-dotenv
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == "dev"
13
+ Requires-Dist: pytest-asyncio; extra == "dev"
14
+ Requires-Dist: respx; extra == "dev"
15
+ Requires-Dist: ruff; extra == "dev"
@@ -0,0 +1,21 @@
1
+ devops_assistant/__init__.py,sha256=66QmS-kBOMDgWObLUqYAKliDhZUYZcwcBFvty5d1OQI,118
2
+ devops_assistant/actions.py,sha256=3O_zrJ0zIPGXLwkFcM6_59sBx38GAfSplFi3_Iz662M,4826
3
+ devops_assistant/app.py,sha256=qBWkdAccUmfrZ33064qakMmfo2i0BB4Q2BKCTIkzF60,3400
4
+ devops_assistant/audit.py,sha256=ZmoLWTT4h-lna08vMQ1ykJGs8HpAryGOOkZGQpxwOGc,3984
5
+ devops_assistant/config.py,sha256=UNiqc2B2MhA37GJHLHCeMkzewVzdZf-iO8AHyYtwNNE,3202
6
+ devops_assistant/errors.py,sha256=g5sv2z-j6VhS7r13BZ_SNSUM3PTXID58SLbVPl5fgx8,496
7
+ devops_assistant/prompts.py,sha256=A4wigi6kL0bAw_CPd2ZkLw2hPQ8lSpjLVFVSXNB50Xg,1502
8
+ devops_assistant/resources.py,sha256=I8pUSmVkKWB42rTFsdvakLdfdRfRtAYE1W2yxV5qcUY,3245
9
+ devops_assistant/security.py,sha256=oCbk_NVMLsBtbK9YjQQkJ29-Gwh7k660HYi8sFZIoJg,717
10
+ devops_assistant/services/__init__.py,sha256=0eSIcwATTcCxDTAL4oFgXAGgymBbMzOJsfTSLTCmEds,32
11
+ devops_assistant/services/docker_client.py,sha256=R4-CB6T-ds5PxVP1YkAplhqCZGkydiRUUJU1mVz6Yzs,10296
12
+ devops_assistant/services/github_client.py,sha256=WNgbD08b9KPr7khjbJC_F9_Ge6OAtCOkSJ9T-bPHDhM,23886
13
+ devops_assistant/tools/__init__.py,sha256=rWBcpxTQpLyH29xIrTwZN8C8y4Bci7_l7y2N7j1jGKA,30
14
+ devops_assistant/tools/docker_read.py,sha256=oKmf8EelwyXJdfD8OUZjFZDzkW9RPS7LuVJw3GkAvPI,2047
15
+ devops_assistant/tools/github_read.py,sha256=oIcIOzwn1y4wkIT4LzXawlJNE2ASClwPPEzL46GntUQ,3422
16
+ devops_assistant/tools/write.py,sha256=7gC2DkVIlqLWVICNNt3JBW0JHNoW8ilkBdZb5zIwHJg,5573
17
+ mcp_devops_assistant-0.1.0.dist-info/METADATA,sha256=2vrHvr9HFpUtFG22q-rKxiq9_nqurgl_99FZqUiFYuE,459
18
+ mcp_devops_assistant-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
19
+ mcp_devops_assistant-0.1.0.dist-info/entry_points.txt,sha256=5UREWGO_j1p_D83R-5ZoNdCTPP4Ip2TDvhztTmWZDxQ,112
20
+ mcp_devops_assistant-0.1.0.dist-info/top_level.txt,sha256=OMFbE8auoO2VIZ91S70-MRAi7gg3800JUvbY_dOHgv4,17
21
+ mcp_devops_assistant-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ devops-assistant = devops_assistant.app:main
3
+ mcp-devops-assistant = devops_assistant.app:main
@@ -0,0 +1 @@
1
+ devops_assistant