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