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/formatters.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Output formatters for MCP responses."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_result(result: dict[str, Any]) -> str:
|
|
8
|
+
"""Format a simple result dictionary.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
result: Result dictionary from operation
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Formatted string for display
|
|
15
|
+
"""
|
|
16
|
+
if result.get("dry_run"):
|
|
17
|
+
output = "DRY RUN MODE - No changes made\n\n"
|
|
18
|
+
output += result.get("message", "")
|
|
19
|
+
if "session_info" in result:
|
|
20
|
+
output += f"\n\nSession Info:\n{json.dumps(result['session_info'], indent=2)}"
|
|
21
|
+
return output
|
|
22
|
+
|
|
23
|
+
return result.get("message", json.dumps(result, indent=2))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_sessions_list(result: dict[str, Any]) -> str:
|
|
27
|
+
"""Format sessions list with filtering info.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
result: Result dictionary with sessions list
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Formatted string for display
|
|
34
|
+
"""
|
|
35
|
+
output = f"Found {result['total']} session(s)"
|
|
36
|
+
|
|
37
|
+
filters = result.get("filters_applied", {})
|
|
38
|
+
if filters:
|
|
39
|
+
output += f"\nFilters applied: {json.dumps(filters, indent=2)}"
|
|
40
|
+
|
|
41
|
+
output += "\n\nSessions:\n"
|
|
42
|
+
|
|
43
|
+
for session in result["sessions"]:
|
|
44
|
+
metadata = session.get("metadata", {})
|
|
45
|
+
spec = session.get("spec", {})
|
|
46
|
+
status = session.get("status", {})
|
|
47
|
+
|
|
48
|
+
name = metadata.get("name", "unknown")
|
|
49
|
+
display_name = spec.get("displayName", "")
|
|
50
|
+
phase = status.get("phase", "unknown")
|
|
51
|
+
created = metadata.get("creationTimestamp", "unknown")
|
|
52
|
+
|
|
53
|
+
output += f"\n- {name}"
|
|
54
|
+
if display_name:
|
|
55
|
+
output += f' ("{display_name}")'
|
|
56
|
+
output += f"\n Status: {phase}\n Created: {created}\n"
|
|
57
|
+
|
|
58
|
+
return output
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def format_bulk_result(result: dict[str, Any], operation: str) -> str:
|
|
62
|
+
"""Format bulk operation results.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
result: Result dictionary from bulk operation
|
|
66
|
+
operation: Operation name (e.g., "delete", "stop")
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Formatted string for display
|
|
70
|
+
"""
|
|
71
|
+
if result.get("dry_run"):
|
|
72
|
+
output = "DRY RUN MODE - No changes made\n\n"
|
|
73
|
+
|
|
74
|
+
# Handle enhanced dry-run for label-based operations
|
|
75
|
+
if "matched_sessions" in result:
|
|
76
|
+
matched = result.get("matched_sessions", [])
|
|
77
|
+
output += f"Matched {result.get('matched_count', len(matched))} sessions with label selector:\n"
|
|
78
|
+
output += f" {result.get('label_selector', 'N/A')}\n\n"
|
|
79
|
+
if matched:
|
|
80
|
+
output += "Matched sessions:\n"
|
|
81
|
+
for session in matched:
|
|
82
|
+
output += f" - {session}\n"
|
|
83
|
+
output += f"\n{result.get('message', '')}\n"
|
|
84
|
+
return output
|
|
85
|
+
|
|
86
|
+
dry_run_info = result.get("dry_run_info", {})
|
|
87
|
+
|
|
88
|
+
would_execute = dry_run_info.get("would_execute", [])
|
|
89
|
+
skipped = dry_run_info.get("skipped", [])
|
|
90
|
+
|
|
91
|
+
if would_execute:
|
|
92
|
+
output += f"Would {operation} {len(would_execute)} session(s):\n"
|
|
93
|
+
for item in would_execute:
|
|
94
|
+
output += f" - {item['session']}\n"
|
|
95
|
+
if item.get("info"):
|
|
96
|
+
info = item["info"]
|
|
97
|
+
if "status" in info:
|
|
98
|
+
output += f" Status: {info['status']}\n"
|
|
99
|
+
|
|
100
|
+
if skipped:
|
|
101
|
+
output += f"\nSkipped ({len(skipped)} session(s)):\n"
|
|
102
|
+
for item in skipped:
|
|
103
|
+
output += f" - {item['session']}"
|
|
104
|
+
if "reason" in item:
|
|
105
|
+
output += f": {item['reason']}"
|
|
106
|
+
output += "\n"
|
|
107
|
+
|
|
108
|
+
return output
|
|
109
|
+
|
|
110
|
+
# Normal mode
|
|
111
|
+
# Map operation to success key
|
|
112
|
+
success_key_map = {
|
|
113
|
+
"delete": "deleted",
|
|
114
|
+
"stop": "stopped",
|
|
115
|
+
"restart": "restarted",
|
|
116
|
+
"label": "labeled",
|
|
117
|
+
"unlabel": "unlabeled",
|
|
118
|
+
}
|
|
119
|
+
success_key = success_key_map.get(operation, operation)
|
|
120
|
+
success = result.get(success_key, [])
|
|
121
|
+
failed = result.get("failed", [])
|
|
122
|
+
|
|
123
|
+
# Determine if we're working with sessions or resources
|
|
124
|
+
resource_type = "session(s)" if "session" in str(failed) else "resource(s)"
|
|
125
|
+
output = f"Successfully {operation}d {len(success)} {resource_type}"
|
|
126
|
+
|
|
127
|
+
if success:
|
|
128
|
+
output += ":\n"
|
|
129
|
+
for session in success:
|
|
130
|
+
output += f" - {session}\n"
|
|
131
|
+
|
|
132
|
+
if failed:
|
|
133
|
+
output += f"\nFailed ({len(failed)} session(s)):\n"
|
|
134
|
+
for item in failed:
|
|
135
|
+
output += f" - {item['session']}: {item['error']}\n"
|
|
136
|
+
|
|
137
|
+
return output
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_logs(result: dict[str, Any]) -> str:
|
|
141
|
+
"""Format session logs.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
result: Result dictionary with logs
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Formatted string for display
|
|
148
|
+
"""
|
|
149
|
+
if "error" in result:
|
|
150
|
+
error_msg = result["error"]
|
|
151
|
+
# Check if this is an expected state rather than an error
|
|
152
|
+
error_lower = error_msg.lower()
|
|
153
|
+
if any(phrase in error_lower for phrase in ["no pods found", "not found", "no running pods"]):
|
|
154
|
+
return f"No logs available: {error_msg}\n\nNote: This is expected for stopped sessions or sessions without active pods."
|
|
155
|
+
return f"Error retrieving logs: {error_msg}"
|
|
156
|
+
|
|
157
|
+
output = f"Logs from container '{result.get('container', 'default')}'"
|
|
158
|
+
output += f" ({result.get('lines', 0)} lines):\n\n"
|
|
159
|
+
output += result.get("logs", "")
|
|
160
|
+
|
|
161
|
+
return output
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def format_clusters(result: dict[str, Any]) -> str:
|
|
165
|
+
"""Format clusters list.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
result: Result dictionary with clusters list
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Formatted string for display
|
|
172
|
+
"""
|
|
173
|
+
clusters = result.get("clusters", [])
|
|
174
|
+
default = result.get("default_cluster")
|
|
175
|
+
|
|
176
|
+
if not clusters:
|
|
177
|
+
return "No clusters configured. Create ~/.config/acp/clusters.yaml to add clusters."
|
|
178
|
+
|
|
179
|
+
output = f"Configured Clusters (default: {default or 'none'}):\n\n"
|
|
180
|
+
|
|
181
|
+
for cluster in clusters:
|
|
182
|
+
name = cluster["name"]
|
|
183
|
+
is_default = cluster.get("is_default", False)
|
|
184
|
+
marker = " [DEFAULT]" if is_default else ""
|
|
185
|
+
|
|
186
|
+
output += f"- {name}{marker}\n"
|
|
187
|
+
output += f" Server: {cluster.get('server', 'N/A')}\n"
|
|
188
|
+
|
|
189
|
+
if cluster.get("description"):
|
|
190
|
+
output += f" Description: {cluster['description']}\n"
|
|
191
|
+
|
|
192
|
+
if cluster.get("default_project"):
|
|
193
|
+
output += f" Default Project: {cluster['default_project']}\n"
|
|
194
|
+
|
|
195
|
+
output += "\n"
|
|
196
|
+
|
|
197
|
+
return output
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def format_whoami(result: dict[str, Any]) -> str:
|
|
201
|
+
"""Format whoami information.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
result: Result dictionary with auth info
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Formatted string for display
|
|
208
|
+
"""
|
|
209
|
+
output = "Current Authentication Status:\n\n"
|
|
210
|
+
|
|
211
|
+
authenticated = result.get("authenticated", False)
|
|
212
|
+
output += f"Authenticated: {'Yes' if authenticated else 'No'}\n"
|
|
213
|
+
|
|
214
|
+
if authenticated:
|
|
215
|
+
output += f"User: {result.get('user', 'unknown')}\n"
|
|
216
|
+
output += f"Cluster: {result.get('cluster', 'unknown')}\n"
|
|
217
|
+
output += f"Server: {result.get('server', 'unknown')}\n"
|
|
218
|
+
output += f"Project: {result.get('project', 'unknown')}\n"
|
|
219
|
+
|
|
220
|
+
token_valid = result.get("token_valid", False)
|
|
221
|
+
output += f"Token Valid: {'Yes' if token_valid else 'No'}\n"
|
|
222
|
+
|
|
223
|
+
if result.get("token_expires"):
|
|
224
|
+
output += f"Token Expires: {result['token_expires']}\n"
|
|
225
|
+
else:
|
|
226
|
+
output += "\nYou are not authenticated. Use 'acp_login' to authenticate.\n"
|
|
227
|
+
|
|
228
|
+
return output
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def format_transcript(result: dict[str, Any]) -> str:
|
|
232
|
+
"""Format session transcript.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
result: Result dictionary with transcript
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Formatted string for display
|
|
239
|
+
"""
|
|
240
|
+
if "error" in result:
|
|
241
|
+
error_msg = result["error"]
|
|
242
|
+
error_lower = error_msg.lower()
|
|
243
|
+
# Check if this is an expected state (no transcript available)
|
|
244
|
+
if any(phrase in error_lower for phrase in ["no transcript", "transcript not found", "no data"]):
|
|
245
|
+
return f"No transcript available: {error_msg}\n\nNote: Sessions may not have transcript data if they are newly created, stopped, or haven't processed messages yet."
|
|
246
|
+
return f"Error retrieving transcript: {error_msg}"
|
|
247
|
+
|
|
248
|
+
format_type = result.get("format", "json")
|
|
249
|
+
message_count = result.get("message_count", 0)
|
|
250
|
+
|
|
251
|
+
if message_count == 0:
|
|
252
|
+
return "Session Transcript: No messages yet.\n\nNote: This session may be newly created or hasn't processed any messages."
|
|
253
|
+
|
|
254
|
+
if format_type == "markdown":
|
|
255
|
+
output = f"Session Transcript ({message_count} messages):\n\n"
|
|
256
|
+
output += result.get("transcript", "")
|
|
257
|
+
return output
|
|
258
|
+
else:
|
|
259
|
+
output = f"Session Transcript ({message_count} messages):\n\n"
|
|
260
|
+
output += json.dumps(result.get("transcript", []), indent=2)
|
|
261
|
+
return output
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def format_metrics(result: dict[str, Any]) -> str:
|
|
265
|
+
"""Format session metrics.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
result: Result dictionary with metrics
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Formatted string for display
|
|
272
|
+
"""
|
|
273
|
+
if "error" in result:
|
|
274
|
+
error_msg = result["error"]
|
|
275
|
+
error_lower = error_msg.lower()
|
|
276
|
+
# Check if this is an expected state (no metrics available)
|
|
277
|
+
if any(phrase in error_lower for phrase in ["no transcript", "no data", "not found"]):
|
|
278
|
+
return f"No metrics available: {error_msg}\n\nNote: Metrics are calculated from transcript data. Sessions without transcript data (new, stopped, or inactive sessions) will not have metrics."
|
|
279
|
+
return f"Error retrieving metrics: {error_msg}"
|
|
280
|
+
|
|
281
|
+
output = "Session Metrics:\n\n"
|
|
282
|
+
message_count = result.get("message_count", 0)
|
|
283
|
+
|
|
284
|
+
if message_count == 0:
|
|
285
|
+
output += "No metrics available yet.\n\nNote: This session has no message history. Metrics will be available after the session processes messages."
|
|
286
|
+
return output
|
|
287
|
+
|
|
288
|
+
output += f"Message Count: {message_count}\n"
|
|
289
|
+
output += f"Token Count (approx): {result.get('token_count', 0)}\n"
|
|
290
|
+
output += f"Duration: {result.get('duration_seconds', 0)} seconds\n"
|
|
291
|
+
output += f"Status: {result.get('status', 'unknown')}\n"
|
|
292
|
+
|
|
293
|
+
tool_calls = result.get("tool_calls", {})
|
|
294
|
+
if tool_calls:
|
|
295
|
+
output += "\nTool Usage:\n"
|
|
296
|
+
for tool_name, count in sorted(tool_calls.items(), key=lambda x: x[1], reverse=True):
|
|
297
|
+
output += f" - {tool_name}: {count}\n"
|
|
298
|
+
|
|
299
|
+
return output
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def format_workflows(result: dict[str, Any]) -> str:
|
|
303
|
+
"""Format workflows list.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
result: Result dictionary with workflows
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Formatted string for display
|
|
310
|
+
"""
|
|
311
|
+
if "error" in result:
|
|
312
|
+
error_msg = result["error"]
|
|
313
|
+
error_lower = error_msg.lower()
|
|
314
|
+
# Check if this is an expected state (no workflows found)
|
|
315
|
+
if any(phrase in error_lower for phrase in ["no workflows", "not found", "no .github/workflows"]):
|
|
316
|
+
return f"No workflows found: {error_msg}\n\nNote: This repository may not have GitHub Actions workflows configured yet."
|
|
317
|
+
return f"Error retrieving workflows: {error_msg}"
|
|
318
|
+
|
|
319
|
+
workflows = result.get("workflows", [])
|
|
320
|
+
repo_url = result.get("repo_url", "")
|
|
321
|
+
count = result.get("count", 0)
|
|
322
|
+
|
|
323
|
+
if not workflows:
|
|
324
|
+
return f"No workflows found in {repo_url}\n\nNote: This repository does not have any GitHub Actions workflows in .github/workflows/"
|
|
325
|
+
|
|
326
|
+
output = f"Available Workflows ({count} found):\n"
|
|
327
|
+
output += f"Repository: {repo_url}\n\n"
|
|
328
|
+
|
|
329
|
+
for workflow in workflows:
|
|
330
|
+
output += f"- {workflow['name']}\n"
|
|
331
|
+
output += f" Path: {workflow['path']}\n"
|
|
332
|
+
if workflow.get("description"):
|
|
333
|
+
output += f" Description: {workflow['description']}\n"
|
|
334
|
+
output += "\n"
|
|
335
|
+
|
|
336
|
+
return output
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def format_export(result: dict[str, Any]) -> str:
|
|
340
|
+
"""Format session export data.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
result: Result dictionary with export data
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Formatted string for display
|
|
347
|
+
"""
|
|
348
|
+
if "error" in result:
|
|
349
|
+
error_msg = result["error"]
|
|
350
|
+
error_lower = error_msg.lower()
|
|
351
|
+
# Check if this is a partial export (some data unavailable)
|
|
352
|
+
if any(phrase in error_lower for phrase in ["no transcript", "no data", "partially exported"]):
|
|
353
|
+
return f"Partial export: {error_msg}\n\nNote: Some session data may be unavailable for stopped or inactive sessions. Exported data reflects what was accessible."
|
|
354
|
+
return f"Error exporting session: {error_msg}"
|
|
355
|
+
|
|
356
|
+
if not result.get("exported"):
|
|
357
|
+
return result.get("message", "Export failed")
|
|
358
|
+
|
|
359
|
+
data = result.get("data", {})
|
|
360
|
+
output = "Session Export:\n\n"
|
|
361
|
+
output += "Configuration:\n"
|
|
362
|
+
output += json.dumps(data.get("config", {}), indent=2)
|
|
363
|
+
output += "\n\nMetadata:\n"
|
|
364
|
+
output += json.dumps(data.get("metadata", {}), indent=2)
|
|
365
|
+
|
|
366
|
+
transcript = data.get("transcript", [])
|
|
367
|
+
transcript_count = len(transcript)
|
|
368
|
+
output += f"\n\nTranscript: {transcript_count} messages"
|
|
369
|
+
|
|
370
|
+
if transcript_count == 0:
|
|
371
|
+
output += " (no transcript data available - this is expected for new/stopped sessions)"
|
|
372
|
+
|
|
373
|
+
output += "\n\n" + result.get("message", "")
|
|
374
|
+
|
|
375
|
+
return output
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def format_cluster_operation(result: dict[str, Any]) -> str:
|
|
379
|
+
"""Format cluster operation results (add, switch, login).
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
result: Result dictionary from cluster operation
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Formatted string for display
|
|
386
|
+
"""
|
|
387
|
+
return result.get("message", json.dumps(result, indent=2))
|