mcp-acp 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.
- mcp_acp/__init__.py +3 -0
- mcp_acp/client.py +1885 -0
- mcp_acp/formatters.py +387 -0
- mcp_acp/server.py +842 -0
- mcp_acp/settings.py +226 -0
- mcp_acp-0.1.0.dist-info/METADATA +446 -0
- mcp_acp-0.1.0.dist-info/RECORD +10 -0
- mcp_acp-0.1.0.dist-info/WHEEL +5 -0
- mcp_acp-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_acp-0.1.0.dist-info/top_level.txt +1 -0
mcp_acp/server.py
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
"""MCP server for Ambient Code Platform management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mcp.server import Server
|
|
9
|
+
from mcp.server.stdio import stdio_server
|
|
10
|
+
from mcp.types import TextContent, Tool
|
|
11
|
+
|
|
12
|
+
from utils.pylogger import get_python_logger
|
|
13
|
+
|
|
14
|
+
from .client import ACPClient
|
|
15
|
+
from .formatters import (
|
|
16
|
+
format_bulk_result,
|
|
17
|
+
format_cluster_operation,
|
|
18
|
+
format_clusters,
|
|
19
|
+
format_export,
|
|
20
|
+
format_logs,
|
|
21
|
+
format_metrics,
|
|
22
|
+
format_result,
|
|
23
|
+
format_sessions_list,
|
|
24
|
+
format_transcript,
|
|
25
|
+
format_whoami,
|
|
26
|
+
format_workflows,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Initialize structured logger
|
|
30
|
+
logger = get_python_logger()
|
|
31
|
+
|
|
32
|
+
# Create MCP server instance
|
|
33
|
+
app = Server("mcp-acp")
|
|
34
|
+
|
|
35
|
+
# Global client instance
|
|
36
|
+
_client: ACPClient | None = None
|
|
37
|
+
|
|
38
|
+
# Schema fragments for reuse
|
|
39
|
+
SCHEMA_FRAGMENTS = {
|
|
40
|
+
"project": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Project/namespace name (optional - uses default_project from clusters.yaml if not provided)",
|
|
43
|
+
},
|
|
44
|
+
"session": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Session name",
|
|
47
|
+
},
|
|
48
|
+
"dry_run": {
|
|
49
|
+
"type": "boolean",
|
|
50
|
+
"description": "Preview without actually executing (default: false)",
|
|
51
|
+
"default": False,
|
|
52
|
+
},
|
|
53
|
+
"sessions_list": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {"type": "string"},
|
|
56
|
+
"description": "List of session names",
|
|
57
|
+
},
|
|
58
|
+
"container": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Container name (e.g., 'runner', 'sidecar')",
|
|
61
|
+
},
|
|
62
|
+
"tail_lines": {
|
|
63
|
+
"type": "integer",
|
|
64
|
+
"description": "Number of lines to retrieve from the end",
|
|
65
|
+
"minimum": 1,
|
|
66
|
+
},
|
|
67
|
+
"display_name": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "Display name for the session",
|
|
70
|
+
},
|
|
71
|
+
"cluster": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Cluster alias name or server URL",
|
|
74
|
+
},
|
|
75
|
+
"repos_list": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": {"type": "string"},
|
|
78
|
+
"description": "List of repository URLs",
|
|
79
|
+
},
|
|
80
|
+
"labels_dict": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"description": "Label key-value pairs (e.g., {'env': 'prod'})",
|
|
83
|
+
"additionalProperties": {"type": "string"},
|
|
84
|
+
},
|
|
85
|
+
"label_keys_list": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": {"type": "string"},
|
|
88
|
+
"description": "List of label keys to remove (without prefix)",
|
|
89
|
+
},
|
|
90
|
+
"resource_type": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Resource type (agenticsession, namespace, etc)",
|
|
93
|
+
},
|
|
94
|
+
"confirm": {
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
"description": "Required for destructive bulk ops (default: false)",
|
|
97
|
+
"default": False,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_tool_schema(properties: dict[str, Any], required: list[str]) -> dict[str, Any]:
|
|
103
|
+
"""Build tool input schema from property references.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
properties: Dict mapping property names to fragment keys or schema dicts
|
|
107
|
+
required: List of required property names
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
JSON schema dict
|
|
111
|
+
"""
|
|
112
|
+
schema_properties = {}
|
|
113
|
+
for prop_name, fragment_key in properties.items():
|
|
114
|
+
if isinstance(fragment_key, str) and fragment_key in SCHEMA_FRAGMENTS:
|
|
115
|
+
# Reference to a schema fragment - use a copy to avoid mutation
|
|
116
|
+
schema_properties[prop_name] = SCHEMA_FRAGMENTS[fragment_key].copy()
|
|
117
|
+
elif isinstance(fragment_key, dict):
|
|
118
|
+
# Inline schema definition - use a copy to avoid mutation
|
|
119
|
+
schema_properties[prop_name] = fragment_key.copy()
|
|
120
|
+
else:
|
|
121
|
+
# String reference not in fragments - treat as-is
|
|
122
|
+
schema_properties[prop_name] = fragment_key
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": schema_properties,
|
|
127
|
+
"required": required,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_client() -> ACPClient:
|
|
132
|
+
"""Get or create ACP client instance with error handling."""
|
|
133
|
+
global _client
|
|
134
|
+
if _client is None:
|
|
135
|
+
config_path = os.getenv("ACP_CLUSTER_CONFIG")
|
|
136
|
+
try:
|
|
137
|
+
logger.info("acp_client_initializing", config_path=config_path or "default")
|
|
138
|
+
_client = ACPClient(config_path=config_path)
|
|
139
|
+
logger.info("acp_client_initialized")
|
|
140
|
+
except ValueError as e:
|
|
141
|
+
logger.error("acp_client_init_failed", error=str(e))
|
|
142
|
+
raise
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error("acp_client_init_unexpected_error", error=str(e), exc_info=True)
|
|
145
|
+
raise
|
|
146
|
+
return _client
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _check_confirmation_then_execute(fn: Callable, args: dict[str, Any], operation: str) -> Any:
|
|
150
|
+
"""Enforce confirmation at server layer (not client).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
fn: Function to execute
|
|
154
|
+
args: Function arguments
|
|
155
|
+
operation: Operation name for error message
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Result from function
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If confirmation not provided for non-dry-run operations
|
|
162
|
+
"""
|
|
163
|
+
if not args.get("dry_run") and not args.get("confirm"):
|
|
164
|
+
raise ValueError(f"Bulk {operation} requires explicit confirmation.\nAdd confirm=true to proceed.")
|
|
165
|
+
return await fn(**args)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.list_tools()
|
|
169
|
+
async def list_tools() -> list[Tool]:
|
|
170
|
+
"""List available ACP (Ambient Code Platform) tools for managing AgenticSession resources on OpenShift/Kubernetes."""
|
|
171
|
+
return [
|
|
172
|
+
# P0 Priority Tools
|
|
173
|
+
Tool(
|
|
174
|
+
name="acp_delete_session",
|
|
175
|
+
description="Delete an ACP (Ambient Code Platform) AgenticSession from an OpenShift project/namespace. Supports dry-run mode for safe preview before deletion.",
|
|
176
|
+
inputSchema=create_tool_schema(
|
|
177
|
+
properties={
|
|
178
|
+
"project": "project",
|
|
179
|
+
"session": "session",
|
|
180
|
+
"dry_run": "dry_run",
|
|
181
|
+
},
|
|
182
|
+
required=["session"],
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
Tool(
|
|
186
|
+
name="acp_list_sessions",
|
|
187
|
+
description="List and filter ACP (Ambient Code Platform) AgenticSessions in an OpenShift project. Filter by status (running/stopped/failed), age, display name, labels. Sort and limit results.",
|
|
188
|
+
inputSchema=create_tool_schema(
|
|
189
|
+
properties={
|
|
190
|
+
"project": "project",
|
|
191
|
+
"status": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "Filter by status",
|
|
194
|
+
"enum": ["running", "stopped", "creating", "failed"],
|
|
195
|
+
},
|
|
196
|
+
"has_display_name": {
|
|
197
|
+
"type": "boolean",
|
|
198
|
+
"description": "Filter by display name presence",
|
|
199
|
+
},
|
|
200
|
+
"older_than": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"description": "Filter by age (e.g., '7d', '24h', '30m')",
|
|
203
|
+
},
|
|
204
|
+
"sort_by": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"description": "Sort field",
|
|
207
|
+
"enum": ["created", "stopped", "name"],
|
|
208
|
+
},
|
|
209
|
+
"limit": {
|
|
210
|
+
"type": "integer",
|
|
211
|
+
"description": "Maximum number of results",
|
|
212
|
+
"minimum": 1,
|
|
213
|
+
},
|
|
214
|
+
"label_selector": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"description": "K8s label selector (e.g., 'acp.ambient-code.ai/label-env=prod,acp.ambient-code.ai/label-team=api')",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
required=[],
|
|
220
|
+
),
|
|
221
|
+
),
|
|
222
|
+
# P1 Priority Tools
|
|
223
|
+
Tool(
|
|
224
|
+
name="acp_restart_session",
|
|
225
|
+
description="Restart a stopped session. Supports dry-run mode.",
|
|
226
|
+
inputSchema=create_tool_schema(
|
|
227
|
+
properties={
|
|
228
|
+
"project": "project",
|
|
229
|
+
"session": "session",
|
|
230
|
+
"dry_run": "dry_run",
|
|
231
|
+
},
|
|
232
|
+
required=["session"],
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
Tool(
|
|
236
|
+
name="acp_bulk_delete_sessions",
|
|
237
|
+
description="Delete multiple sessions (max 3). DESTRUCTIVE: requires confirm=true. Use dry_run=true first!",
|
|
238
|
+
inputSchema=create_tool_schema(
|
|
239
|
+
properties={
|
|
240
|
+
"project": "project",
|
|
241
|
+
"sessions": "sessions_list",
|
|
242
|
+
"confirm": "confirm",
|
|
243
|
+
"dry_run": "dry_run",
|
|
244
|
+
},
|
|
245
|
+
required=["sessions"],
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
Tool(
|
|
249
|
+
name="acp_bulk_stop_sessions",
|
|
250
|
+
description="Stop multiple running sessions (max 3). Requires confirm=true. Use dry_run=true first!",
|
|
251
|
+
inputSchema=create_tool_schema(
|
|
252
|
+
properties={
|
|
253
|
+
"project": "project",
|
|
254
|
+
"sessions": "sessions_list",
|
|
255
|
+
"confirm": "confirm",
|
|
256
|
+
"dry_run": "dry_run",
|
|
257
|
+
},
|
|
258
|
+
required=["sessions"],
|
|
259
|
+
),
|
|
260
|
+
),
|
|
261
|
+
Tool(
|
|
262
|
+
name="acp_get_session_logs",
|
|
263
|
+
description="Retrieve container logs for a session for debugging purposes.",
|
|
264
|
+
inputSchema=create_tool_schema(
|
|
265
|
+
properties={
|
|
266
|
+
"project": "project",
|
|
267
|
+
"session": "session",
|
|
268
|
+
"container": "container",
|
|
269
|
+
"tail_lines": "tail_lines",
|
|
270
|
+
},
|
|
271
|
+
required=["session"],
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
Tool(
|
|
275
|
+
name="acp_list_clusters",
|
|
276
|
+
description="List configured cluster aliases from clusters.yaml configuration.",
|
|
277
|
+
inputSchema={"type": "object", "properties": {}},
|
|
278
|
+
),
|
|
279
|
+
Tool(
|
|
280
|
+
name="acp_whoami",
|
|
281
|
+
description="Get current authentication status and user information.",
|
|
282
|
+
inputSchema={"type": "object", "properties": {}},
|
|
283
|
+
),
|
|
284
|
+
# Label Management Tools
|
|
285
|
+
Tool(
|
|
286
|
+
name="acp_label_resource",
|
|
287
|
+
description="Add/update labels on any ACP resource. Works for sessions, workspaces, future types. Uses --overwrite.",
|
|
288
|
+
inputSchema=create_tool_schema(
|
|
289
|
+
properties={
|
|
290
|
+
"resource_type": "resource_type",
|
|
291
|
+
"name": "session",
|
|
292
|
+
"project": "project",
|
|
293
|
+
"labels": "labels_dict",
|
|
294
|
+
"dry_run": "dry_run",
|
|
295
|
+
},
|
|
296
|
+
required=["resource_type", "name", "project", "labels"],
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
Tool(
|
|
300
|
+
name="acp_unlabel_resource",
|
|
301
|
+
description="Remove specific labels from any ACP resource.",
|
|
302
|
+
inputSchema=create_tool_schema(
|
|
303
|
+
properties={
|
|
304
|
+
"resource_type": "resource_type",
|
|
305
|
+
"name": "session",
|
|
306
|
+
"project": "project",
|
|
307
|
+
"label_keys": "label_keys_list",
|
|
308
|
+
"dry_run": "dry_run",
|
|
309
|
+
},
|
|
310
|
+
required=["resource_type", "name", "project", "label_keys"],
|
|
311
|
+
),
|
|
312
|
+
),
|
|
313
|
+
Tool(
|
|
314
|
+
name="acp_bulk_label_resources",
|
|
315
|
+
description="Label multiple resources (max 3) with same labels. Requires confirm=true.",
|
|
316
|
+
inputSchema=create_tool_schema(
|
|
317
|
+
properties={
|
|
318
|
+
"resource_type": "resource_type",
|
|
319
|
+
"names": "sessions_list",
|
|
320
|
+
"project": "project",
|
|
321
|
+
"labels": "labels_dict",
|
|
322
|
+
"confirm": "confirm",
|
|
323
|
+
"dry_run": "dry_run",
|
|
324
|
+
},
|
|
325
|
+
required=["resource_type", "names", "project", "labels"],
|
|
326
|
+
),
|
|
327
|
+
),
|
|
328
|
+
Tool(
|
|
329
|
+
name="acp_bulk_unlabel_resources",
|
|
330
|
+
description="Remove labels from multiple resources (max 3). Requires confirm=true.",
|
|
331
|
+
inputSchema=create_tool_schema(
|
|
332
|
+
properties={
|
|
333
|
+
"resource_type": "resource_type",
|
|
334
|
+
"names": "sessions_list",
|
|
335
|
+
"project": "project",
|
|
336
|
+
"label_keys": "label_keys_list",
|
|
337
|
+
"confirm": "confirm",
|
|
338
|
+
"dry_run": "dry_run",
|
|
339
|
+
},
|
|
340
|
+
required=["resource_type", "names", "project", "label_keys"],
|
|
341
|
+
),
|
|
342
|
+
),
|
|
343
|
+
Tool(
|
|
344
|
+
name="acp_list_sessions_by_label",
|
|
345
|
+
description="List sessions filtered by user-friendly labels (convenience wrapper, auto-prefixes labels).",
|
|
346
|
+
inputSchema=create_tool_schema(
|
|
347
|
+
properties={
|
|
348
|
+
"project": "project",
|
|
349
|
+
"labels": "labels_dict",
|
|
350
|
+
"status": {
|
|
351
|
+
"type": "string",
|
|
352
|
+
"description": "Filter by status (running, stopped, etc)",
|
|
353
|
+
},
|
|
354
|
+
"limit": {
|
|
355
|
+
"type": "integer",
|
|
356
|
+
"description": "Limit results",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
required=["project", "labels"],
|
|
360
|
+
),
|
|
361
|
+
),
|
|
362
|
+
Tool(
|
|
363
|
+
name="acp_bulk_delete_sessions_by_label",
|
|
364
|
+
description="Delete sessions (max 3) matching label selector. DESTRUCTIVE: requires confirm=true.",
|
|
365
|
+
inputSchema=create_tool_schema(
|
|
366
|
+
properties={
|
|
367
|
+
"project": "project",
|
|
368
|
+
"labels": "labels_dict",
|
|
369
|
+
"confirm": "confirm",
|
|
370
|
+
"dry_run": "dry_run",
|
|
371
|
+
},
|
|
372
|
+
required=["project", "labels"],
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
Tool(
|
|
376
|
+
name="acp_bulk_stop_sessions_by_label",
|
|
377
|
+
description="Stop sessions (max 3) matching label selector. Requires confirm=true.",
|
|
378
|
+
inputSchema=create_tool_schema(
|
|
379
|
+
properties={
|
|
380
|
+
"project": "project",
|
|
381
|
+
"labels": "labels_dict",
|
|
382
|
+
"confirm": "confirm",
|
|
383
|
+
"dry_run": "dry_run",
|
|
384
|
+
},
|
|
385
|
+
required=["project", "labels"],
|
|
386
|
+
),
|
|
387
|
+
),
|
|
388
|
+
Tool(
|
|
389
|
+
name="acp_bulk_restart_sessions",
|
|
390
|
+
description="Restart multiple stopped sessions (max 3). Requires confirm=true.",
|
|
391
|
+
inputSchema=create_tool_schema(
|
|
392
|
+
properties={
|
|
393
|
+
"project": "project",
|
|
394
|
+
"sessions": "sessions_list",
|
|
395
|
+
"confirm": "confirm",
|
|
396
|
+
"dry_run": "dry_run",
|
|
397
|
+
},
|
|
398
|
+
required=["project", "sessions"],
|
|
399
|
+
),
|
|
400
|
+
),
|
|
401
|
+
Tool(
|
|
402
|
+
name="acp_bulk_restart_sessions_by_label",
|
|
403
|
+
description="Restart sessions (max 3) matching label selector. Requires confirm=true.",
|
|
404
|
+
inputSchema=create_tool_schema(
|
|
405
|
+
properties={
|
|
406
|
+
"project": "project",
|
|
407
|
+
"labels": "labels_dict",
|
|
408
|
+
"confirm": "confirm",
|
|
409
|
+
"dry_run": "dry_run",
|
|
410
|
+
},
|
|
411
|
+
required=["project", "labels"],
|
|
412
|
+
),
|
|
413
|
+
),
|
|
414
|
+
# P2 Priority Tools
|
|
415
|
+
Tool(
|
|
416
|
+
name="acp_clone_session",
|
|
417
|
+
description="Clone a session with its configuration. Supports dry-run mode.",
|
|
418
|
+
inputSchema=create_tool_schema(
|
|
419
|
+
properties={
|
|
420
|
+
"project": "project",
|
|
421
|
+
"source_session": "session",
|
|
422
|
+
"new_display_name": "display_name",
|
|
423
|
+
"dry_run": "dry_run",
|
|
424
|
+
},
|
|
425
|
+
required=["source_session", "new_display_name"],
|
|
426
|
+
),
|
|
427
|
+
),
|
|
428
|
+
Tool(
|
|
429
|
+
name="acp_get_session_transcript",
|
|
430
|
+
description="Get session transcript/conversation history.",
|
|
431
|
+
inputSchema=create_tool_schema(
|
|
432
|
+
properties={
|
|
433
|
+
"project": "project",
|
|
434
|
+
"session": "session",
|
|
435
|
+
"format": {
|
|
436
|
+
"type": "string",
|
|
437
|
+
"description": "Output format",
|
|
438
|
+
"enum": ["json", "markdown"],
|
|
439
|
+
"default": "json",
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
required=["session"],
|
|
443
|
+
),
|
|
444
|
+
),
|
|
445
|
+
Tool(
|
|
446
|
+
name="acp_update_session",
|
|
447
|
+
description="Update session metadata (display name, timeout). Supports dry-run mode.",
|
|
448
|
+
inputSchema=create_tool_schema(
|
|
449
|
+
properties={
|
|
450
|
+
"project": "project",
|
|
451
|
+
"session": "session",
|
|
452
|
+
"display_name": "display_name",
|
|
453
|
+
"timeout": {
|
|
454
|
+
"type": "integer",
|
|
455
|
+
"description": "Timeout in seconds",
|
|
456
|
+
},
|
|
457
|
+
"dry_run": "dry_run",
|
|
458
|
+
},
|
|
459
|
+
required=["session"],
|
|
460
|
+
),
|
|
461
|
+
),
|
|
462
|
+
Tool(
|
|
463
|
+
name="acp_export_session",
|
|
464
|
+
description="Export session configuration and transcript for archival.",
|
|
465
|
+
inputSchema=create_tool_schema(
|
|
466
|
+
properties={
|
|
467
|
+
"project": "project",
|
|
468
|
+
"session": "session",
|
|
469
|
+
},
|
|
470
|
+
required=["session"],
|
|
471
|
+
),
|
|
472
|
+
),
|
|
473
|
+
# P3 Priority Tools
|
|
474
|
+
Tool(
|
|
475
|
+
name="acp_get_session_metrics",
|
|
476
|
+
description="Get session metrics (token usage, duration, tool calls).",
|
|
477
|
+
inputSchema=create_tool_schema(
|
|
478
|
+
properties={
|
|
479
|
+
"project": "project",
|
|
480
|
+
"session": "session",
|
|
481
|
+
},
|
|
482
|
+
required=["session"],
|
|
483
|
+
),
|
|
484
|
+
),
|
|
485
|
+
Tool(
|
|
486
|
+
name="acp_list_workflows",
|
|
487
|
+
description="List available workflows from repository.",
|
|
488
|
+
inputSchema=create_tool_schema(
|
|
489
|
+
properties={
|
|
490
|
+
"repo_url": {
|
|
491
|
+
"type": "string",
|
|
492
|
+
"description": "Repository URL (defaults to ootb-ambient-workflows)",
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
required=[],
|
|
496
|
+
),
|
|
497
|
+
),
|
|
498
|
+
Tool(
|
|
499
|
+
name="acp_create_session_from_template",
|
|
500
|
+
description="Create session from predefined template (triage, bugfix, feature, exploration). Supports dry-run mode.",
|
|
501
|
+
inputSchema=create_tool_schema(
|
|
502
|
+
properties={
|
|
503
|
+
"project": "project",
|
|
504
|
+
"template": {
|
|
505
|
+
"type": "string",
|
|
506
|
+
"description": "Template name",
|
|
507
|
+
"enum": ["triage", "bugfix", "feature", "exploration"],
|
|
508
|
+
},
|
|
509
|
+
"display_name": "display_name",
|
|
510
|
+
"repos": "repos_list",
|
|
511
|
+
"dry_run": "dry_run",
|
|
512
|
+
},
|
|
513
|
+
required=["template", "display_name"],
|
|
514
|
+
),
|
|
515
|
+
),
|
|
516
|
+
# Auth Enhancement Tools
|
|
517
|
+
Tool(
|
|
518
|
+
name="acp_login",
|
|
519
|
+
description="Authenticate to OpenShift cluster via web or token.",
|
|
520
|
+
inputSchema=create_tool_schema(
|
|
521
|
+
properties={
|
|
522
|
+
"cluster": "cluster",
|
|
523
|
+
"web": {
|
|
524
|
+
"type": "boolean",
|
|
525
|
+
"description": "Use web login flow (default: true)",
|
|
526
|
+
"default": True,
|
|
527
|
+
},
|
|
528
|
+
"token": {
|
|
529
|
+
"type": "string",
|
|
530
|
+
"description": "Direct token for authentication",
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
required=["cluster"],
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
Tool(
|
|
537
|
+
name="acp_switch_cluster",
|
|
538
|
+
description="Switch to a different cluster context.",
|
|
539
|
+
inputSchema=create_tool_schema(
|
|
540
|
+
properties={
|
|
541
|
+
"cluster": "cluster",
|
|
542
|
+
},
|
|
543
|
+
required=["cluster"],
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
Tool(
|
|
547
|
+
name="acp_add_cluster",
|
|
548
|
+
description="Add a new cluster to configuration.",
|
|
549
|
+
inputSchema=create_tool_schema(
|
|
550
|
+
properties={
|
|
551
|
+
"name": {
|
|
552
|
+
"type": "string",
|
|
553
|
+
"description": "Cluster alias name",
|
|
554
|
+
},
|
|
555
|
+
"server": {
|
|
556
|
+
"type": "string",
|
|
557
|
+
"description": "Server URL",
|
|
558
|
+
},
|
|
559
|
+
"description": {
|
|
560
|
+
"type": "string",
|
|
561
|
+
"description": "Optional description",
|
|
562
|
+
},
|
|
563
|
+
"default_project": {
|
|
564
|
+
"type": "string",
|
|
565
|
+
"description": "Optional default project",
|
|
566
|
+
},
|
|
567
|
+
"set_default": {
|
|
568
|
+
"type": "boolean",
|
|
569
|
+
"description": "Set as default cluster",
|
|
570
|
+
"default": False,
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
required=["name", "server"],
|
|
574
|
+
),
|
|
575
|
+
),
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# Async wrapper functions for confirmation-protected bulk operations
|
|
580
|
+
def create_bulk_wrappers(client: ACPClient) -> dict[str, Callable]:
|
|
581
|
+
"""Create async wrapper functions for bulk operations with confirmation.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
client: ACP client instance
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
Dict of wrapper function names to async functions
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
async def bulk_delete_wrapper(**args):
|
|
591
|
+
return await _check_confirmation_then_execute(client.bulk_delete_sessions, args, "delete")
|
|
592
|
+
|
|
593
|
+
async def bulk_stop_wrapper(**args):
|
|
594
|
+
return await _check_confirmation_then_execute(client.bulk_stop_sessions, args, "stop")
|
|
595
|
+
|
|
596
|
+
async def bulk_delete_by_label_wrapper(**args):
|
|
597
|
+
return await _check_confirmation_then_execute(client.bulk_delete_sessions_by_label, args, "delete")
|
|
598
|
+
|
|
599
|
+
async def bulk_stop_by_label_wrapper(**args):
|
|
600
|
+
return await _check_confirmation_then_execute(client.bulk_stop_sessions_by_label, args, "stop")
|
|
601
|
+
|
|
602
|
+
async def bulk_restart_wrapper(**args):
|
|
603
|
+
return await _check_confirmation_then_execute(client.bulk_restart_sessions, args, "restart")
|
|
604
|
+
|
|
605
|
+
async def bulk_restart_by_label_wrapper(**args):
|
|
606
|
+
return await _check_confirmation_then_execute(client.bulk_restart_sessions_by_label, args, "restart")
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
"bulk_delete": bulk_delete_wrapper,
|
|
610
|
+
"bulk_stop": bulk_stop_wrapper,
|
|
611
|
+
"bulk_delete_by_label": bulk_delete_by_label_wrapper,
|
|
612
|
+
"bulk_stop_by_label": bulk_stop_by_label_wrapper,
|
|
613
|
+
"bulk_restart": bulk_restart_wrapper,
|
|
614
|
+
"bulk_restart_by_label": bulk_restart_by_label_wrapper,
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# Tool dispatch table: maps tool names to (handler, formatter) pairs
|
|
619
|
+
def create_dispatch_table(client: ACPClient) -> dict[str, tuple[Callable, Callable]]:
|
|
620
|
+
"""Create tool dispatch table.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
client: ACP client instance
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Dict mapping tool names to (handler, formatter) tuples
|
|
627
|
+
"""
|
|
628
|
+
bulk_wrappers = create_bulk_wrappers(client)
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
"acp_delete_session": (
|
|
632
|
+
client.delete_session,
|
|
633
|
+
format_result,
|
|
634
|
+
),
|
|
635
|
+
"acp_list_sessions": (
|
|
636
|
+
client.list_sessions,
|
|
637
|
+
format_sessions_list,
|
|
638
|
+
),
|
|
639
|
+
"acp_restart_session": (
|
|
640
|
+
client.restart_session,
|
|
641
|
+
format_result,
|
|
642
|
+
),
|
|
643
|
+
"acp_bulk_delete_sessions": (
|
|
644
|
+
bulk_wrappers["bulk_delete"],
|
|
645
|
+
lambda r: format_bulk_result(r, "delete"),
|
|
646
|
+
),
|
|
647
|
+
"acp_bulk_stop_sessions": (
|
|
648
|
+
bulk_wrappers["bulk_stop"],
|
|
649
|
+
lambda r: format_bulk_result(r, "stop"),
|
|
650
|
+
),
|
|
651
|
+
"acp_get_session_logs": (
|
|
652
|
+
client.get_session_logs,
|
|
653
|
+
format_logs,
|
|
654
|
+
),
|
|
655
|
+
"acp_list_clusters": (
|
|
656
|
+
client.list_clusters,
|
|
657
|
+
format_clusters,
|
|
658
|
+
),
|
|
659
|
+
"acp_whoami": (
|
|
660
|
+
client.whoami,
|
|
661
|
+
format_whoami,
|
|
662
|
+
),
|
|
663
|
+
# Label Management Tools
|
|
664
|
+
"acp_label_resource": (
|
|
665
|
+
client.label_resource,
|
|
666
|
+
format_result,
|
|
667
|
+
),
|
|
668
|
+
"acp_unlabel_resource": (
|
|
669
|
+
client.unlabel_resource,
|
|
670
|
+
format_result,
|
|
671
|
+
),
|
|
672
|
+
"acp_bulk_label_resources": (
|
|
673
|
+
lambda **args: _check_confirmation_then_execute(client.bulk_label_resources, args, "label"),
|
|
674
|
+
lambda r: format_bulk_result(r, "label"),
|
|
675
|
+
),
|
|
676
|
+
"acp_bulk_unlabel_resources": (
|
|
677
|
+
lambda **args: _check_confirmation_then_execute(client.bulk_unlabel_resources, args, "unlabel"),
|
|
678
|
+
lambda r: format_bulk_result(r, "unlabel"),
|
|
679
|
+
),
|
|
680
|
+
"acp_list_sessions_by_label": (
|
|
681
|
+
client.list_sessions_by_user_labels,
|
|
682
|
+
format_sessions_list,
|
|
683
|
+
),
|
|
684
|
+
"acp_bulk_delete_sessions_by_label": (
|
|
685
|
+
bulk_wrappers["bulk_delete_by_label"],
|
|
686
|
+
lambda r: format_bulk_result(r, "delete"),
|
|
687
|
+
),
|
|
688
|
+
"acp_bulk_stop_sessions_by_label": (
|
|
689
|
+
bulk_wrappers["bulk_stop_by_label"],
|
|
690
|
+
lambda r: format_bulk_result(r, "stop"),
|
|
691
|
+
),
|
|
692
|
+
"acp_bulk_restart_sessions": (
|
|
693
|
+
bulk_wrappers["bulk_restart"],
|
|
694
|
+
lambda r: format_bulk_result(r, "restart"),
|
|
695
|
+
),
|
|
696
|
+
"acp_bulk_restart_sessions_by_label": (
|
|
697
|
+
bulk_wrappers["bulk_restart_by_label"],
|
|
698
|
+
lambda r: format_bulk_result(r, "restart"),
|
|
699
|
+
),
|
|
700
|
+
# P2 Tools
|
|
701
|
+
"acp_clone_session": (
|
|
702
|
+
client.clone_session,
|
|
703
|
+
format_result,
|
|
704
|
+
),
|
|
705
|
+
"acp_get_session_transcript": (
|
|
706
|
+
client.get_session_transcript,
|
|
707
|
+
format_transcript,
|
|
708
|
+
),
|
|
709
|
+
"acp_update_session": (
|
|
710
|
+
client.update_session,
|
|
711
|
+
format_result,
|
|
712
|
+
),
|
|
713
|
+
"acp_export_session": (
|
|
714
|
+
client.export_session,
|
|
715
|
+
format_export,
|
|
716
|
+
),
|
|
717
|
+
# P3 Tools
|
|
718
|
+
"acp_get_session_metrics": (
|
|
719
|
+
client.get_session_metrics,
|
|
720
|
+
format_metrics,
|
|
721
|
+
),
|
|
722
|
+
"acp_list_workflows": (
|
|
723
|
+
client.list_workflows,
|
|
724
|
+
format_workflows,
|
|
725
|
+
),
|
|
726
|
+
"acp_create_session_from_template": (
|
|
727
|
+
client.create_session_from_template,
|
|
728
|
+
format_result,
|
|
729
|
+
),
|
|
730
|
+
# Auth Tools
|
|
731
|
+
"acp_login": (
|
|
732
|
+
client.login,
|
|
733
|
+
format_cluster_operation,
|
|
734
|
+
),
|
|
735
|
+
"acp_switch_cluster": (
|
|
736
|
+
client.switch_cluster,
|
|
737
|
+
format_cluster_operation,
|
|
738
|
+
),
|
|
739
|
+
"acp_add_cluster": (
|
|
740
|
+
client.add_cluster,
|
|
741
|
+
format_cluster_operation,
|
|
742
|
+
),
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@app.call_tool()
|
|
747
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
748
|
+
"""Handle tool calls with dispatch table.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
name: Tool name
|
|
752
|
+
arguments: Tool arguments
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
List of text content responses
|
|
756
|
+
"""
|
|
757
|
+
import time
|
|
758
|
+
|
|
759
|
+
start_time = time.time()
|
|
760
|
+
|
|
761
|
+
# Security: Sanitize arguments for logging (remove sensitive data)
|
|
762
|
+
safe_args = {k: v for k, v in arguments.items() if k not in ["token", "password", "secret"]}
|
|
763
|
+
logger.info("tool_call_started", tool=name, arguments=safe_args)
|
|
764
|
+
|
|
765
|
+
client = get_client()
|
|
766
|
+
dispatch_table = create_dispatch_table(client)
|
|
767
|
+
|
|
768
|
+
try:
|
|
769
|
+
handler, formatter = dispatch_table.get(name, (None, None))
|
|
770
|
+
|
|
771
|
+
if not handler:
|
|
772
|
+
logger.warning("unknown_tool_requested", tool=name)
|
|
773
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
774
|
+
|
|
775
|
+
# Auto-fill project from default_project if not provided or empty
|
|
776
|
+
if not arguments.get("project"):
|
|
777
|
+
# Get default project from current cluster config
|
|
778
|
+
default_cluster = client.config.get("default_cluster")
|
|
779
|
+
if default_cluster:
|
|
780
|
+
cluster_config = client.config.get("clusters", {}).get(default_cluster, {})
|
|
781
|
+
default_project = cluster_config.get("default_project")
|
|
782
|
+
if default_project:
|
|
783
|
+
arguments["project"] = default_project
|
|
784
|
+
logger.info("project_autofilled", project=default_project, cluster=default_cluster)
|
|
785
|
+
|
|
786
|
+
# Call handler (async or sync)
|
|
787
|
+
if asyncio.iscoroutinefunction(handler):
|
|
788
|
+
result = await handler(**arguments)
|
|
789
|
+
else:
|
|
790
|
+
result = handler(**arguments)
|
|
791
|
+
|
|
792
|
+
# Log execution time
|
|
793
|
+
elapsed = time.time() - start_time
|
|
794
|
+
logger.info("tool_call_completed", tool=name, elapsed_seconds=round(elapsed, 2))
|
|
795
|
+
|
|
796
|
+
# Check for errors in result
|
|
797
|
+
if isinstance(result, dict):
|
|
798
|
+
if result.get("error"):
|
|
799
|
+
logger.warning("tool_returned_error", tool=name, error=result.get("error"))
|
|
800
|
+
elif not result.get("success", True) and "message" in result:
|
|
801
|
+
logger.warning("tool_failed", tool=name, message=result.get("message"))
|
|
802
|
+
|
|
803
|
+
return [TextContent(type="text", text=formatter(result))]
|
|
804
|
+
|
|
805
|
+
except ValueError as e:
|
|
806
|
+
# Validation errors - these are expected for invalid input
|
|
807
|
+
elapsed = time.time() - start_time
|
|
808
|
+
logger.warning("tool_validation_error", tool=name, elapsed_seconds=round(elapsed, 2), error=str(e))
|
|
809
|
+
return [TextContent(type="text", text=f"Validation Error: {str(e)}")]
|
|
810
|
+
except TimeoutError as e:
|
|
811
|
+
elapsed = time.time() - start_time
|
|
812
|
+
logger.error("tool_timeout", tool=name, elapsed_seconds=round(elapsed, 2), error=str(e))
|
|
813
|
+
return [TextContent(type="text", text=f"Timeout Error: {str(e)}")]
|
|
814
|
+
except Exception as e:
|
|
815
|
+
elapsed = time.time() - start_time
|
|
816
|
+
logger.error(
|
|
817
|
+
"tool_unexpected_error",
|
|
818
|
+
tool=name,
|
|
819
|
+
elapsed_seconds=round(elapsed, 2),
|
|
820
|
+
error=str(e),
|
|
821
|
+
exc_info=True,
|
|
822
|
+
)
|
|
823
|
+
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
async def main() -> None:
|
|
827
|
+
"""Run the MCP server."""
|
|
828
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
829
|
+
await app.run(
|
|
830
|
+
read_stream,
|
|
831
|
+
write_stream,
|
|
832
|
+
app.create_initialization_options(),
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def run() -> None:
|
|
837
|
+
"""Entry point for the MCP server."""
|
|
838
|
+
asyncio.run(main())
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
if __name__ == "__main__":
|
|
842
|
+
run()
|