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/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()