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.
- devops_assistant/__init__.py +5 -0
- devops_assistant/actions.py +150 -0
- devops_assistant/app.py +96 -0
- devops_assistant/audit.py +129 -0
- devops_assistant/config.py +78 -0
- devops_assistant/errors.py +21 -0
- devops_assistant/prompts.py +28 -0
- devops_assistant/resources.py +96 -0
- devops_assistant/security.py +26 -0
- devops_assistant/services/__init__.py +1 -0
- devops_assistant/services/docker_client.py +259 -0
- devops_assistant/services/github_client.py +587 -0
- devops_assistant/tools/__init__.py +1 -0
- devops_assistant/tools/docker_read.py +57 -0
- devops_assistant/tools/github_read.py +102 -0
- devops_assistant/tools/write.py +161 -0
- mcp_devops_assistant-0.1.0.dist-info/METADATA +15 -0
- mcp_devops_assistant-0.1.0.dist-info/RECORD +21 -0
- mcp_devops_assistant-0.1.0.dist-info/WHEEL +5 -0
- mcp_devops_assistant-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_devops_assistant-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
1
|
+
devops_assistant
|