dockerhub-api 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.
- dockerhub_api/__init__.py +80 -0
- dockerhub_api/__main__.py +4 -0
- dockerhub_api/agent_server.py +92 -0
- dockerhub_api/api/__init__.py +1 -0
- dockerhub_api/api/api_client_access_tokens.py +77 -0
- dockerhub_api/api/api_client_audit_logs.py +56 -0
- dockerhub_api/api/api_client_auth.py +80 -0
- dockerhub_api/api/api_client_base.py +338 -0
- dockerhub_api/api/api_client_groups.py +158 -0
- dockerhub_api/api/api_client_org_access_tokens.py +106 -0
- dockerhub_api/api/api_client_orgs.py +149 -0
- dockerhub_api/api/api_client_repositories.py +217 -0
- dockerhub_api/api/api_client_scim.py +153 -0
- dockerhub_api/api_client.py +35 -0
- dockerhub_api/auth.py +252 -0
- dockerhub_api/dockerhub_input_models.py +756 -0
- dockerhub_api/dockerhub_response_models.py +344 -0
- dockerhub_api/main_agent.json +14 -0
- dockerhub_api/mcp/__init__.py +120 -0
- dockerhub_api/mcp/mcp_admin.py +45 -0
- dockerhub_api/mcp/mcp_audit.py +44 -0
- dockerhub_api/mcp/mcp_auth.py +66 -0
- dockerhub_api/mcp/mcp_org.py +58 -0
- dockerhub_api/mcp/mcp_repos.py +58 -0
- dockerhub_api/mcp/mcp_scim.py +56 -0
- dockerhub_api/mcp/mcp_teams.py +55 -0
- dockerhub_api/mcp_config.json +3 -0
- dockerhub_api/mcp_server.py +109 -0
- dockerhub_api-0.1.0.dist-info/METADATA +230 -0
- dockerhub_api-0.1.0.dist-info/RECORD +34 -0
- dockerhub_api-0.1.0.dist-info/WHEEL +5 -0
- dockerhub_api-0.1.0.dist-info/entry_points.txt +3 -0
- dockerhub_api-0.1.0.dist-info/licenses/LICENSE +21 -0
- dockerhub_api-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Pydantic response models for the Docker Hub API client.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.0 — core wrapper.
|
|
4
|
+
|
|
5
|
+
Models are intentionally lenient (``extra="allow"``, optional fields) so they
|
|
6
|
+
survive Docker Hub schema evolution: known fields get typed, unknown fields
|
|
7
|
+
ride along. Client methods validate response data against these models when
|
|
8
|
+
the shape is known and fall back to raw JSON when validation fails.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HubModel(BaseModel):
|
|
17
|
+
model_config = ConfigDict(extra="allow")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RateLimit(HubModel):
|
|
21
|
+
"""Snapshot of the ``X-RateLimit-*`` headers from the latest response."""
|
|
22
|
+
|
|
23
|
+
limit: int | None = Field(default=None, description="Request ceiling")
|
|
24
|
+
remaining: int | None = Field(default=None, description="Requests remaining")
|
|
25
|
+
reset: int | None = Field(default=None, description="Window reset (epoch s)")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Page(HubModel):
|
|
29
|
+
"""Standard Docker Hub page envelope (``count``/``next``/``previous``)."""
|
|
30
|
+
|
|
31
|
+
count: int | None = None
|
|
32
|
+
next: str | None = None
|
|
33
|
+
previous: str | None = None
|
|
34
|
+
results: list[Any] | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class JwtToken(HubModel):
|
|
38
|
+
"""``POST /v2/auth/token`` response."""
|
|
39
|
+
|
|
40
|
+
access_token: str | None = None
|
|
41
|
+
token: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LoginResult(HubModel):
|
|
45
|
+
"""``POST /v2/users/login`` / ``POST /v2/users/2fa-login`` response."""
|
|
46
|
+
|
|
47
|
+
token: str | None = None
|
|
48
|
+
refresh_token: str | None = None
|
|
49
|
+
detail: str | None = None
|
|
50
|
+
login_2fa_token: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AccessToken(HubModel):
|
|
54
|
+
"""A personal access token record."""
|
|
55
|
+
|
|
56
|
+
uuid: str | None = None
|
|
57
|
+
token_label: str | None = None
|
|
58
|
+
token: str | None = Field(
|
|
59
|
+
default=None, description="Plaintext token — only present on creation"
|
|
60
|
+
)
|
|
61
|
+
scopes: list[str] | None = None
|
|
62
|
+
is_active: bool | None = None
|
|
63
|
+
client_id: str | None = None
|
|
64
|
+
creator_ip: str | None = None
|
|
65
|
+
creator_ua: str | None = None
|
|
66
|
+
created_at: str | None = None
|
|
67
|
+
last_used: str | None = None
|
|
68
|
+
generated_by: str | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AccessTokenPage(Page):
|
|
72
|
+
active_count: int | None = None
|
|
73
|
+
results: list[AccessToken] | None = None # type: ignore[assignment]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OrgAccessToken(HubModel):
|
|
77
|
+
"""An organization access token record."""
|
|
78
|
+
|
|
79
|
+
id: str | None = None
|
|
80
|
+
uuid: str | None = None
|
|
81
|
+
label: str | None = None
|
|
82
|
+
description: str | None = None
|
|
83
|
+
token: str | None = Field(
|
|
84
|
+
default=None, description="Plaintext token — only present on creation"
|
|
85
|
+
)
|
|
86
|
+
scopes: list[str] | None = None
|
|
87
|
+
resources: list[dict] | None = None
|
|
88
|
+
is_active: bool | None = None
|
|
89
|
+
created_at: str | None = None
|
|
90
|
+
expires_at: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class OrgAccessTokenPage(Page):
|
|
94
|
+
results: list[OrgAccessToken] | None = None # type: ignore[assignment]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AuditLogEvent(HubModel):
|
|
98
|
+
"""One audit-log entry."""
|
|
99
|
+
|
|
100
|
+
account: str | None = None
|
|
101
|
+
action: str | None = None
|
|
102
|
+
name: str | None = None
|
|
103
|
+
actor: str | None = None
|
|
104
|
+
actor_ip: str | None = None
|
|
105
|
+
data: dict | None = None
|
|
106
|
+
timestamp: str | None = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AuditLogPage(HubModel):
|
|
110
|
+
logs: list[AuditLogEvent] | None = None
|
|
111
|
+
count: int | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AuditLogActions(HubModel):
|
|
115
|
+
"""``GET /v2/auditlogs/{account}/actions`` response."""
|
|
116
|
+
|
|
117
|
+
actions: dict | list | None = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RestrictedImages(HubModel):
|
|
121
|
+
enabled: bool | None = None
|
|
122
|
+
allow_official_images: bool | None = None
|
|
123
|
+
allow_verified_publishers: bool | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class OrgSettings(HubModel):
|
|
127
|
+
"""``GET|PUT /v2/orgs/{name}/settings`` response."""
|
|
128
|
+
|
|
129
|
+
restricted_images: RestrictedImages | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Repository(HubModel):
|
|
133
|
+
"""A Docker Hub image repository."""
|
|
134
|
+
|
|
135
|
+
name: str | None = None
|
|
136
|
+
namespace: str | None = None
|
|
137
|
+
repository_type: str | None = None
|
|
138
|
+
description: str | None = None
|
|
139
|
+
full_description: str | None = None
|
|
140
|
+
registry: str | None = None
|
|
141
|
+
is_private: bool | None = None
|
|
142
|
+
status: int | str | None = None
|
|
143
|
+
star_count: int | None = None
|
|
144
|
+
pull_count: int | None = None
|
|
145
|
+
last_updated: str | None = None
|
|
146
|
+
date_registered: str | None = None
|
|
147
|
+
affiliation: str | None = None
|
|
148
|
+
media_types: list[str] | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class RepositoryPage(Page):
|
|
152
|
+
results: list[Repository] | None = None # type: ignore[assignment]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TagImage(HubModel):
|
|
156
|
+
architecture: str | None = None
|
|
157
|
+
os: str | None = None
|
|
158
|
+
digest: str | None = None
|
|
159
|
+
size: int | None = None
|
|
160
|
+
status: str | None = None
|
|
161
|
+
last_pushed: str | None = None
|
|
162
|
+
last_pulled: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Tag(HubModel):
|
|
166
|
+
"""A repository tag."""
|
|
167
|
+
|
|
168
|
+
id: int | None = None
|
|
169
|
+
name: str | None = None
|
|
170
|
+
repository: int | str | None = None
|
|
171
|
+
full_size: int | None = None
|
|
172
|
+
digest: str | None = None
|
|
173
|
+
images: list[TagImage] | None = None
|
|
174
|
+
last_updated: str | None = None
|
|
175
|
+
last_updater_username: str | None = None
|
|
176
|
+
tag_status: str | None = None
|
|
177
|
+
tag_last_pulled: str | None = None
|
|
178
|
+
tag_last_pushed: str | None = None
|
|
179
|
+
v2: bool | str | None = None
|
|
180
|
+
content_type: str | None = None
|
|
181
|
+
media_type: str | None = None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TagPage(Page):
|
|
185
|
+
results: list[Tag] | None = None # type: ignore[assignment]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ImmutableTagsSettings(HubModel):
|
|
189
|
+
"""``PATCH .../immutabletags`` response."""
|
|
190
|
+
|
|
191
|
+
enabled: bool | None = None
|
|
192
|
+
rules: list[str] | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ImmutableTagsVerification(HubModel):
|
|
196
|
+
"""``POST .../immutabletags/verify`` response."""
|
|
197
|
+
|
|
198
|
+
enabled: bool | None = None
|
|
199
|
+
rules: list[str] | None = None
|
|
200
|
+
results: list[dict] | None = None
|
|
201
|
+
errors: list[dict] | None = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class RepositoryGroup(HubModel):
|
|
205
|
+
"""``POST /v2/repositories/{ns}/{repo}/groups`` response."""
|
|
206
|
+
|
|
207
|
+
group_id: int | str | None = None
|
|
208
|
+
group_name: str | None = None
|
|
209
|
+
permission: str | None = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class OrgMember(HubModel):
|
|
213
|
+
"""An organization member."""
|
|
214
|
+
|
|
215
|
+
id: str | None = None
|
|
216
|
+
username: str | None = None
|
|
217
|
+
full_name: str | None = None
|
|
218
|
+
email: str | None = None
|
|
219
|
+
role: str | None = None
|
|
220
|
+
type: str | None = None
|
|
221
|
+
groups: list[str] | None = None
|
|
222
|
+
is_guest: bool | None = None
|
|
223
|
+
last_logged_in_date: str | None = None
|
|
224
|
+
joined_at: str | None = None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class OrgMemberPage(Page):
|
|
228
|
+
results: list[OrgMember] | None = None # type: ignore[assignment]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Invite(HubModel):
|
|
232
|
+
"""An organization invite."""
|
|
233
|
+
|
|
234
|
+
id: str | None = None
|
|
235
|
+
invitee: str | None = None
|
|
236
|
+
inviter: str | None = None
|
|
237
|
+
org: str | None = None
|
|
238
|
+
team: str | None = None
|
|
239
|
+
role: str | None = None
|
|
240
|
+
created_at: str | None = None
|
|
241
|
+
status: str | None = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class InvitePage(Page):
|
|
245
|
+
data: list[Invite] | None = None
|
|
246
|
+
results: list[Invite] | None = None # type: ignore[assignment]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class BulkInviteResult(HubModel):
|
|
250
|
+
"""``POST /v2/invites/bulk`` response."""
|
|
251
|
+
|
|
252
|
+
org: str | None = None
|
|
253
|
+
team: str | None = None
|
|
254
|
+
role: str | None = None
|
|
255
|
+
dry_run: bool | None = None
|
|
256
|
+
invitees: list[dict] | list[str] | None = None
|
|
257
|
+
valid: list[Any] | None = None
|
|
258
|
+
invalid: list[Any] | None = None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Group(HubModel):
|
|
262
|
+
"""An organization group (team)."""
|
|
263
|
+
|
|
264
|
+
id: int | str | None = None
|
|
265
|
+
name: str | None = None
|
|
266
|
+
description: str | None = None
|
|
267
|
+
member_count: int | None = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class GroupPage(Page):
|
|
271
|
+
results: list[Group] | None = None # type: ignore[assignment]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class ScimName(HubModel):
|
|
275
|
+
givenName: str | None = None
|
|
276
|
+
familyName: str | None = None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class ScimEmail(HubModel):
|
|
280
|
+
value: str | None = None
|
|
281
|
+
primary: bool | None = None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class ScimUser(HubModel):
|
|
285
|
+
"""A SCIM 2.0 User resource."""
|
|
286
|
+
|
|
287
|
+
schemas: list[str] | None = None
|
|
288
|
+
id: str | None = None
|
|
289
|
+
userName: str | None = None
|
|
290
|
+
name: ScimName | None = None
|
|
291
|
+
emails: list[ScimEmail] | None = None
|
|
292
|
+
active: bool | None = None
|
|
293
|
+
groups: list[dict] | None = None
|
|
294
|
+
meta: dict | None = None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class ScimListResponse(HubModel):
|
|
298
|
+
"""SCIM 2.0 list envelope (``ListResponse``)."""
|
|
299
|
+
|
|
300
|
+
schemas: list[str] | None = None
|
|
301
|
+
totalResults: int | None = None
|
|
302
|
+
startIndex: int | None = None
|
|
303
|
+
itemsPerPage: int | None = None
|
|
304
|
+
Resources: list[dict] | None = None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ScimServiceProviderConfig(HubModel):
|
|
308
|
+
schemas: list[str] | None = None
|
|
309
|
+
patch: dict | None = None
|
|
310
|
+
bulk: dict | None = None
|
|
311
|
+
filter: dict | None = None
|
|
312
|
+
changePassword: dict | None = None
|
|
313
|
+
sort: dict | None = None
|
|
314
|
+
etag: dict | None = None
|
|
315
|
+
authenticationSchemes: list[dict] | None = None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class ScimResourceType(HubModel):
|
|
319
|
+
schemas: list[str] | None = None
|
|
320
|
+
id: str | None = None
|
|
321
|
+
name: str | None = None
|
|
322
|
+
endpoint: str | None = None
|
|
323
|
+
schema_uri: str | None = Field(default=None, alias="schema")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ScimSchema(HubModel):
|
|
327
|
+
id: str | None = None
|
|
328
|
+
name: str | None = None
|
|
329
|
+
description: str | None = None
|
|
330
|
+
attributes: list[dict] | None = None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def validate_lenient(model: type[BaseModel], data: Any) -> Any:
|
|
334
|
+
"""Validate ``data`` against ``model``; fall back to the raw data.
|
|
335
|
+
|
|
336
|
+
Keeps client results JSON-serializable while still exercising the typed
|
|
337
|
+
models whenever the payload matches the documented shape.
|
|
338
|
+
"""
|
|
339
|
+
if data is None:
|
|
340
|
+
return None
|
|
341
|
+
try:
|
|
342
|
+
return model.model_validate(data).model_dump(mode="json", exclude_none=True)
|
|
343
|
+
except Exception: # pragma: no cover - lenient by design
|
|
344
|
+
return data
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"task": "main-agent",
|
|
3
|
+
"input": "# main-agent\n\nYou are the Docker Hub operations agent for this workspace. Your goal is to help the user manage Docker Hub repositories, tags, access tokens, organizations, teams, audit logs, and SCIM provisioning.\n\n### Core Principles\n* Be concise and efficient.\n* Prefer read-only inspection before any write.\n* Destructive operations (deletes, org-settings writes) are gated behind DOCKERHUB_ALLOW_DESTRUCTIVE — confirm intent before enabling.\n* Verify your work before concluding.\n\nYour personality:\n* **Emoji:** 🐳\n* **Vibe:** Professional, efficient, helpful",
|
|
4
|
+
"type": "prompt",
|
|
5
|
+
"description": "The Docker Hub operations agent for this workspace.",
|
|
6
|
+
"tools": [
|
|
7
|
+
"workspace-manager",
|
|
8
|
+
"agent-workflows"
|
|
9
|
+
],
|
|
10
|
+
"topic": "Docker Hub Administration",
|
|
11
|
+
"tone": "technical and precise",
|
|
12
|
+
"style": "professional assistant",
|
|
13
|
+
"goal": "Manage Docker Hub repositories, tokens, organizations, teams, and provisioning."
|
|
14
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""MCP tool registration modules for dockerhub-api.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.4 — action-routed MCP surface. Each module registers one
|
|
4
|
+
consolidated, action-routed tool; this package provides the shared client
|
|
5
|
+
resolution, parameter parsing, secret redaction, and error-envelope helpers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from agent_utilities.core.exceptions import (
|
|
12
|
+
ApiError,
|
|
13
|
+
AuthError,
|
|
14
|
+
MissingParameterError,
|
|
15
|
+
ParameterError,
|
|
16
|
+
UnauthorizedError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from dockerhub_api.api.api_client_base import DestructiveOperationError
|
|
20
|
+
|
|
21
|
+
#: Keys whose values are always masked in MCP tool results.
|
|
22
|
+
SECRET_KEYS = {"secret", "password", "access_token", "refresh_token", "jwt"}
|
|
23
|
+
REDACTED = "***redacted***"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_hub_client():
|
|
27
|
+
"""Resolve the Docker Hub client late so tests/deployments can rebind
|
|
28
|
+
``dockerhub_api.auth.get_client``."""
|
|
29
|
+
from dockerhub_api.auth import get_client
|
|
30
|
+
|
|
31
|
+
return get_client()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_params(params_json: str) -> dict[str, Any]:
|
|
35
|
+
"""Parse the ``params_json`` tool argument, dropping ``None`` values."""
|
|
36
|
+
kwargs = json.loads(params_json) if params_json else {}
|
|
37
|
+
if not isinstance(kwargs, dict):
|
|
38
|
+
raise ValueError("params_json must decode to a JSON object")
|
|
39
|
+
return {k: v for k, v in kwargs.items() if v is not None}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def redact_secrets(data: Any, allow_keys: set[str] | None = None) -> Any:
|
|
43
|
+
"""Recursively mask secret-bearing fields in a tool result.
|
|
44
|
+
|
|
45
|
+
``allow_keys`` whitelists keys that must stay visible for this one call
|
|
46
|
+
(e.g. the plaintext ``token`` returned exactly once on PAT creation).
|
|
47
|
+
"""
|
|
48
|
+
allow = allow_keys or set()
|
|
49
|
+
if isinstance(data, dict):
|
|
50
|
+
return {
|
|
51
|
+
key: (
|
|
52
|
+
REDACTED
|
|
53
|
+
if key in SECRET_KEYS and key not in allow and value
|
|
54
|
+
else redact_secrets(value, allow)
|
|
55
|
+
)
|
|
56
|
+
for key, value in data.items()
|
|
57
|
+
}
|
|
58
|
+
if isinstance(data, list):
|
|
59
|
+
return [redact_secrets(item, allow) for item in data]
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def error_envelope(error: Exception) -> dict[str, Any]:
|
|
64
|
+
"""Map a client exception to the MCP error envelope."""
|
|
65
|
+
return {"error": {"type": type(error).__name__, "message": str(error)}}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
HANDLED_ERRORS = (
|
|
69
|
+
ApiError,
|
|
70
|
+
AuthError,
|
|
71
|
+
UnauthorizedError,
|
|
72
|
+
ParameterError,
|
|
73
|
+
MissingParameterError,
|
|
74
|
+
DestructiveOperationError,
|
|
75
|
+
ValueError,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_action(handlers: dict, action: str, kwargs: dict[str, Any]) -> Any:
|
|
80
|
+
"""Dispatch ``action`` to its handler, mapping errors to the envelope."""
|
|
81
|
+
handler = handlers.get(action)
|
|
82
|
+
if handler is None:
|
|
83
|
+
return {
|
|
84
|
+
"error": {
|
|
85
|
+
"type": "UnknownAction",
|
|
86
|
+
"message": f"Unknown action: {action!r}. "
|
|
87
|
+
f"Valid actions: {sorted(handlers)}",
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
try:
|
|
91
|
+
return handler(**kwargs)
|
|
92
|
+
except HANDLED_ERRORS as error:
|
|
93
|
+
return error_envelope(error)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
from dockerhub_api.mcp.mcp_admin import register_admin_tools
|
|
97
|
+
from dockerhub_api.mcp.mcp_audit import register_audit_tools
|
|
98
|
+
from dockerhub_api.mcp.mcp_auth import register_auth_tools
|
|
99
|
+
from dockerhub_api.mcp.mcp_org import register_org_tools
|
|
100
|
+
from dockerhub_api.mcp.mcp_repos import register_repos_tools
|
|
101
|
+
from dockerhub_api.mcp.mcp_scim import register_scim_tools
|
|
102
|
+
from dockerhub_api.mcp.mcp_teams import register_teams_tools
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"HANDLED_ERRORS",
|
|
106
|
+
"REDACTED",
|
|
107
|
+
"SECRET_KEYS",
|
|
108
|
+
"error_envelope",
|
|
109
|
+
"get_hub_client",
|
|
110
|
+
"parse_params",
|
|
111
|
+
"redact_secrets",
|
|
112
|
+
"register_admin_tools",
|
|
113
|
+
"register_audit_tools",
|
|
114
|
+
"register_auth_tools",
|
|
115
|
+
"register_org_tools",
|
|
116
|
+
"register_repos_tools",
|
|
117
|
+
"register_scim_tools",
|
|
118
|
+
"register_teams_tools",
|
|
119
|
+
"run_action",
|
|
120
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""MCP tool for Docker Hub client telemetry and identity introspection.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.4 — action-routed MCP surface.
|
|
4
|
+
CONCEPT:HUB-1.2 — rate-limit telemetry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastmcp import Context, FastMCP
|
|
10
|
+
from fastmcp.dependencies import Depends
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_admin_tools(mcp: FastMCP):
|
|
17
|
+
@mcp.tool(tags={"admin"})
|
|
18
|
+
async def hub_admin(
|
|
19
|
+
action: str = Field(
|
|
20
|
+
description="Action to perform. Must be one of: 'rate_limit', 'whoami'"
|
|
21
|
+
),
|
|
22
|
+
params_json: str = Field(
|
|
23
|
+
default="{}", description="JSON string of parameters to pass to the action."
|
|
24
|
+
),
|
|
25
|
+
client=Depends(get_hub_client),
|
|
26
|
+
ctx: Context | None = Field(
|
|
27
|
+
default=None, description="MCP context for progress reporting"
|
|
28
|
+
),
|
|
29
|
+
) -> Any:
|
|
30
|
+
"""Client diagnostics: 'rate_limit' returns the latest
|
|
31
|
+
X-RateLimit-* snapshot observed by the client; 'whoami' introspects
|
|
32
|
+
the active credential locally (decoded JWT claims, no network call).
|
|
33
|
+
"""
|
|
34
|
+
if ctx:
|
|
35
|
+
await ctx.info("Executing tool...")
|
|
36
|
+
try:
|
|
37
|
+
kwargs = parse_params(params_json)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return {"error": f"Invalid params_json: {e}"}
|
|
40
|
+
|
|
41
|
+
handlers = {
|
|
42
|
+
"rate_limit": client.get_rate_limit,
|
|
43
|
+
"whoami": client.whoami,
|
|
44
|
+
}
|
|
45
|
+
return redact_secrets(run_action(handlers, action, kwargs))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""MCP tool for Docker Hub audit logs.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.4 — action-routed MCP surface.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastmcp import Context, FastMCP
|
|
9
|
+
from fastmcp.dependencies import Depends
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_audit_tools(mcp: FastMCP):
|
|
16
|
+
@mcp.tool(tags={"audit"})
|
|
17
|
+
async def hub_audit(
|
|
18
|
+
action: str = Field(
|
|
19
|
+
description="Action to perform. Must be one of: 'logs', 'actions'"
|
|
20
|
+
),
|
|
21
|
+
params_json: str = Field(
|
|
22
|
+
default="{}", description="JSON string of parameters to pass to the action."
|
|
23
|
+
),
|
|
24
|
+
client=Depends(get_hub_client),
|
|
25
|
+
ctx: Context | None = Field(
|
|
26
|
+
default=None, description="MCP context for progress reporting"
|
|
27
|
+
),
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Read a Docker Hub account's audit trail: 'logs' lists events
|
|
30
|
+
(filters: action, name, actor, from_date, to_date, page, page_size),
|
|
31
|
+
'actions' lists the available action names.
|
|
32
|
+
"""
|
|
33
|
+
if ctx:
|
|
34
|
+
await ctx.info("Executing tool...")
|
|
35
|
+
try:
|
|
36
|
+
kwargs = parse_params(params_json)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return {"error": f"Invalid params_json: {e}"}
|
|
39
|
+
|
|
40
|
+
handlers = {
|
|
41
|
+
"logs": client.get_audit_logs,
|
|
42
|
+
"actions": client.get_audit_log_actions,
|
|
43
|
+
}
|
|
44
|
+
return redact_secrets(run_action(handlers, action, kwargs))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""MCP tool for Docker Hub auth and token lifecycle.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.4 — action-routed MCP surface.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastmcp import Context, FastMCP
|
|
9
|
+
from fastmcp.dependencies import Depends
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_auth_tools(mcp: FastMCP):
|
|
16
|
+
@mcp.tool(tags={"auth"})
|
|
17
|
+
async def hub_auth(
|
|
18
|
+
action: str = Field(
|
|
19
|
+
description=(
|
|
20
|
+
"Action to perform. Must be one of: 'create_token', 'login' "
|
|
21
|
+
"(deprecated), 'two_factor_login', 'list_pats', 'create_pat', "
|
|
22
|
+
"'get_pat', 'update_pat', 'delete_pat', 'list_oats', "
|
|
23
|
+
"'create_oat', 'get_oat', 'update_oat', 'delete_oat'"
|
|
24
|
+
)
|
|
25
|
+
),
|
|
26
|
+
params_json: str = Field(
|
|
27
|
+
default="{}", description="JSON string of parameters to pass to the action."
|
|
28
|
+
),
|
|
29
|
+
client=Depends(get_hub_client),
|
|
30
|
+
ctx: Context | None = Field(
|
|
31
|
+
default=None, description="MCP context for progress reporting"
|
|
32
|
+
),
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""Manage Docker Hub authentication, personal access tokens (PATs),
|
|
35
|
+
and organization access tokens (OATs).
|
|
36
|
+
|
|
37
|
+
Bearer JWTs are minted/cached client-side and redacted from results.
|
|
38
|
+
Plaintext PAT/OAT values appear exactly once, on creation. Token
|
|
39
|
+
deletion requires DOCKERHUB_ALLOW_DESTRUCTIVE=True.
|
|
40
|
+
"""
|
|
41
|
+
if ctx:
|
|
42
|
+
await ctx.info("Executing tool...")
|
|
43
|
+
try:
|
|
44
|
+
kwargs = parse_params(params_json)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return {"error": f"Invalid params_json: {e}"}
|
|
47
|
+
|
|
48
|
+
handlers = {
|
|
49
|
+
"create_token": client.create_auth_token,
|
|
50
|
+
"login": client.login,
|
|
51
|
+
"two_factor_login": client.two_factor_login,
|
|
52
|
+
"list_pats": client.get_access_tokens,
|
|
53
|
+
"create_pat": client.create_access_token,
|
|
54
|
+
"get_pat": client.get_access_token,
|
|
55
|
+
"update_pat": client.update_access_token,
|
|
56
|
+
"delete_pat": client.delete_access_token,
|
|
57
|
+
"list_oats": client.get_org_access_tokens,
|
|
58
|
+
"create_oat": client.create_org_access_token,
|
|
59
|
+
"get_oat": client.get_org_access_token,
|
|
60
|
+
"update_oat": client.update_org_access_token,
|
|
61
|
+
"delete_oat": client.delete_org_access_token,
|
|
62
|
+
}
|
|
63
|
+
result = run_action(handlers, action, kwargs)
|
|
64
|
+
# Plaintext tokens are shown exactly once — on creation.
|
|
65
|
+
allow_keys = {"token"} if action in ("create_pat", "create_oat") else set()
|
|
66
|
+
return redact_secrets(result, allow_keys=allow_keys)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""MCP tool for Docker Hub organization members, settings, and invites.
|
|
2
|
+
|
|
3
|
+
CONCEPT:HUB-1.4 — action-routed MCP surface.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastmcp import Context, FastMCP
|
|
9
|
+
from fastmcp.dependencies import Depends
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from dockerhub_api.mcp import get_hub_client, parse_params, redact_secrets, run_action
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_org_tools(mcp: FastMCP):
|
|
16
|
+
@mcp.tool(tags={"org"})
|
|
17
|
+
async def hub_org(
|
|
18
|
+
action: str = Field(
|
|
19
|
+
description=(
|
|
20
|
+
"Action to perform. Must be one of: 'get_settings', "
|
|
21
|
+
"'update_settings', 'list_members', 'export_members', "
|
|
22
|
+
"'update_member', 'remove_member', 'list_invites', "
|
|
23
|
+
"'delete_invite', 'resend_invite', 'bulk_invite'"
|
|
24
|
+
)
|
|
25
|
+
),
|
|
26
|
+
params_json: str = Field(
|
|
27
|
+
default="{}", description="JSON string of parameters to pass to the action."
|
|
28
|
+
),
|
|
29
|
+
client=Depends(get_hub_client),
|
|
30
|
+
ctx: Context | None = Field(
|
|
31
|
+
default=None, description="MCP context for progress reporting"
|
|
32
|
+
),
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""Manage a Docker Hub organization: settings (restricted images),
|
|
35
|
+
member roster and roles, CSV export, and invites (single, resend,
|
|
36
|
+
bulk with dry_run). Member removal, invite deletion, and settings
|
|
37
|
+
writes require DOCKERHUB_ALLOW_DESTRUCTIVE=True.
|
|
38
|
+
"""
|
|
39
|
+
if ctx:
|
|
40
|
+
await ctx.info("Executing tool...")
|
|
41
|
+
try:
|
|
42
|
+
kwargs = parse_params(params_json)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
return {"error": f"Invalid params_json: {e}"}
|
|
45
|
+
|
|
46
|
+
handlers = {
|
|
47
|
+
"get_settings": client.get_org_settings,
|
|
48
|
+
"update_settings": client.update_org_settings,
|
|
49
|
+
"list_members": client.get_org_members,
|
|
50
|
+
"export_members": client.export_org_members,
|
|
51
|
+
"update_member": client.update_org_member,
|
|
52
|
+
"remove_member": client.remove_org_member,
|
|
53
|
+
"list_invites": client.get_org_invites,
|
|
54
|
+
"delete_invite": client.delete_invite,
|
|
55
|
+
"resend_invite": client.resend_invite,
|
|
56
|
+
"bulk_invite": client.bulk_invite,
|
|
57
|
+
}
|
|
58
|
+
return redact_secrets(run_action(handlers, action, kwargs))
|