kubectl-mcp-server 1.13.0__py3-none-any.whl → 1.15.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.
- kubectl_mcp_server-1.15.0.dist-info/METADATA +1026 -0
- kubectl_mcp_server-1.15.0.dist-info/RECORD +49 -0
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/__init__.py +53 -1
- kubectl_mcp_tool/cli/cli.py +476 -17
- kubectl_mcp_tool/cli/errors.py +262 -0
- kubectl_mcp_tool/cli/output.py +377 -0
- kubectl_mcp_tool/k8s_config.py +285 -63
- kubectl_mcp_tool/tools/browser.py +316 -28
- kubectl_mcp_tool/tools/cluster.py +395 -121
- kubectl_mcp_tool/tools/core.py +157 -60
- kubectl_mcp_tool/tools/cost.py +97 -41
- kubectl_mcp_tool/tools/deployments.py +173 -56
- kubectl_mcp_tool/tools/diagnostics.py +40 -13
- kubectl_mcp_tool/tools/helm.py +133 -46
- kubectl_mcp_tool/tools/networking.py +106 -32
- kubectl_mcp_tool/tools/operations.py +176 -50
- kubectl_mcp_tool/tools/pods.py +162 -50
- kubectl_mcp_tool/tools/security.py +89 -36
- kubectl_mcp_tool/tools/storage.py +35 -16
- tests/test_browser.py +167 -5
- tests/test_cli.py +299 -0
- tests/test_tools.py +10 -9
- kubectl_mcp_server-1.13.0.dist-info/METADATA +0 -780
- kubectl_mcp_server-1.13.0.dist-info/RECORD +0 -46
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.15.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.15.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.15.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.15.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Structured error handling for kubectl-mcp-server CLI."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorCode(IntEnum):
|
|
9
|
+
SUCCESS = 0
|
|
10
|
+
CLIENT_ERROR = 1
|
|
11
|
+
SERVER_ERROR = 2
|
|
12
|
+
K8S_ERROR = 3
|
|
13
|
+
BROWSER_ERROR = 4
|
|
14
|
+
NETWORK_ERROR = 5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CliError:
|
|
19
|
+
code: ErrorCode
|
|
20
|
+
type: str
|
|
21
|
+
message: str
|
|
22
|
+
details: Optional[str] = None
|
|
23
|
+
suggestion: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return format_cli_error(self)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_cli_error(error: CliError) -> str:
|
|
30
|
+
lines = [f"Error [{error.type}]: {error.message}"]
|
|
31
|
+
if error.details:
|
|
32
|
+
lines.append(f" Details: {error.details}")
|
|
33
|
+
if error.suggestion:
|
|
34
|
+
lines.append(f" Suggestion: {error.suggestion}")
|
|
35
|
+
return "\n".join(lines)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def tool_not_found_error(name: str, available: Optional[List[str]] = None) -> CliError:
|
|
39
|
+
available = available or []
|
|
40
|
+
available_str = ", ".join(available[:5])
|
|
41
|
+
if len(available) > 5:
|
|
42
|
+
available_str += f" (+{len(available) - 5} more)"
|
|
43
|
+
|
|
44
|
+
return CliError(
|
|
45
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
46
|
+
type="TOOL_NOT_FOUND",
|
|
47
|
+
message=f'Tool "{name}" not found',
|
|
48
|
+
details=f"Available: {available_str}" if available else None,
|
|
49
|
+
suggestion="Run 'kubectl-mcp-server tools' to see all available tools"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def tool_execution_error(name: str, cause: str) -> CliError:
|
|
54
|
+
suggestion = "Check tool arguments match the expected schema"
|
|
55
|
+
|
|
56
|
+
if "validation" in cause.lower() or "invalid" in cause.lower():
|
|
57
|
+
suggestion = f"Run 'kubectl-mcp-server tools {name}' to see the input schema"
|
|
58
|
+
elif "required" in cause.lower():
|
|
59
|
+
suggestion = f"Missing required argument. Run 'kubectl-mcp-server tools {name}' for schema"
|
|
60
|
+
elif "permission" in cause.lower() or "denied" in cause.lower():
|
|
61
|
+
suggestion = "Permission denied. Check RBAC permissions in your cluster"
|
|
62
|
+
|
|
63
|
+
return CliError(
|
|
64
|
+
code=ErrorCode.SERVER_ERROR,
|
|
65
|
+
type="TOOL_EXECUTION_FAILED",
|
|
66
|
+
message=f'Tool "{name}" execution failed',
|
|
67
|
+
details=cause,
|
|
68
|
+
suggestion=suggestion
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def k8s_connection_error(context: str, cause: str) -> CliError:
|
|
73
|
+
suggestion = "Check your kubeconfig and ensure the cluster is accessible"
|
|
74
|
+
|
|
75
|
+
if "refused" in cause.lower():
|
|
76
|
+
suggestion = "Connection refused. Is the cluster running? Check 'kubectl cluster-info'"
|
|
77
|
+
elif "timeout" in cause.lower():
|
|
78
|
+
suggestion = "Connection timed out. Check network connectivity to the cluster"
|
|
79
|
+
elif "unauthorized" in cause.lower() or "401" in cause:
|
|
80
|
+
suggestion = "Authentication failed. Check your credentials with 'kubectl auth whoami'"
|
|
81
|
+
|
|
82
|
+
return CliError(
|
|
83
|
+
code=ErrorCode.K8S_ERROR,
|
|
84
|
+
type="K8S_CONNECTION_FAILED",
|
|
85
|
+
message=f'Failed to connect to Kubernetes context "{context}"',
|
|
86
|
+
details=cause,
|
|
87
|
+
suggestion=suggestion
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def k8s_context_error(context: str, available: Optional[List[str]] = None) -> CliError:
|
|
92
|
+
available = available or []
|
|
93
|
+
available_str = ", ".join(available[:5]) if available else "(none)"
|
|
94
|
+
|
|
95
|
+
return CliError(
|
|
96
|
+
code=ErrorCode.K8S_ERROR,
|
|
97
|
+
type="K8S_CONTEXT_NOT_FOUND",
|
|
98
|
+
message=f'Kubernetes context "{context}" not found',
|
|
99
|
+
details=f"Available contexts: {available_str}",
|
|
100
|
+
suggestion="Run 'kubectl config get-contexts' to list available contexts"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def k8s_resource_error(resource: str, namespace: str, cause: str) -> CliError:
|
|
105
|
+
return CliError(
|
|
106
|
+
code=ErrorCode.K8S_ERROR,
|
|
107
|
+
type="K8S_RESOURCE_ERROR",
|
|
108
|
+
message=f'Failed to access {resource} in namespace "{namespace}"',
|
|
109
|
+
details=cause,
|
|
110
|
+
suggestion="Check if the resource exists and you have permission to access it"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def browser_not_found_error() -> CliError:
|
|
115
|
+
return CliError(
|
|
116
|
+
code=ErrorCode.BROWSER_ERROR,
|
|
117
|
+
type="BROWSER_NOT_FOUND",
|
|
118
|
+
message="agent-browser CLI not found",
|
|
119
|
+
details="Browser automation tools require agent-browser to be installed",
|
|
120
|
+
suggestion="Install with: npm install -g agent-browser && agent-browser install"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def browser_not_enabled_error() -> CliError:
|
|
125
|
+
return CliError(
|
|
126
|
+
code=ErrorCode.BROWSER_ERROR,
|
|
127
|
+
type="BROWSER_NOT_ENABLED",
|
|
128
|
+
message="Browser tools are not enabled",
|
|
129
|
+
details="Set MCP_BROWSER_ENABLED=true to enable browser automation",
|
|
130
|
+
suggestion="Export MCP_BROWSER_ENABLED=true and ensure agent-browser is installed"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def browser_connection_error(cause: str) -> CliError:
|
|
135
|
+
suggestion = "Check browser configuration and try again"
|
|
136
|
+
|
|
137
|
+
if "ECONNREFUSED" in cause:
|
|
138
|
+
suggestion = "Connection refused. Is the browser running? Try 'agent-browser install'"
|
|
139
|
+
elif "timeout" in cause.lower():
|
|
140
|
+
suggestion = "Connection timed out. Try increasing MCP_BROWSER_TIMEOUT"
|
|
141
|
+
elif "CDP" in cause or "websocket" in cause.lower():
|
|
142
|
+
suggestion = "CDP connection failed. Check MCP_BROWSER_CDP_URL is correct"
|
|
143
|
+
|
|
144
|
+
return CliError(
|
|
145
|
+
code=ErrorCode.BROWSER_ERROR,
|
|
146
|
+
type="BROWSER_CONNECTION_FAILED",
|
|
147
|
+
message="Failed to connect to browser",
|
|
148
|
+
details=cause,
|
|
149
|
+
suggestion=suggestion
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def browser_timeout_error(timeout: int) -> CliError:
|
|
154
|
+
return CliError(
|
|
155
|
+
code=ErrorCode.BROWSER_ERROR,
|
|
156
|
+
type="BROWSER_TIMEOUT",
|
|
157
|
+
message=f"Browser command timed out after {timeout}s",
|
|
158
|
+
details="The command took too long to complete",
|
|
159
|
+
suggestion="Increase timeout or check if the page is loading correctly"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def invalid_json_error(input_str: str, parse_error: str) -> CliError:
|
|
164
|
+
truncated = input_str[:100] + "..." if len(input_str) > 100 else input_str
|
|
165
|
+
|
|
166
|
+
return CliError(
|
|
167
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
168
|
+
type="INVALID_JSON",
|
|
169
|
+
message="Invalid JSON in arguments",
|
|
170
|
+
details=f"Parse error: {parse_error}",
|
|
171
|
+
suggestion=f"Use valid JSON format: '{{\"key\": \"value\"}}'. Input was: {truncated}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def missing_argument_error(command: str, argument: str) -> CliError:
|
|
176
|
+
suggestions = {
|
|
177
|
+
"call": "Use 'kubectl-mcp-server call <tool> '{\"key\": \"value\"}'",
|
|
178
|
+
"tools": "Use 'kubectl-mcp-server tools <name>' to inspect a tool",
|
|
179
|
+
"grep": "Use 'kubectl-mcp-server grep \"*pattern*\"' to search tools",
|
|
180
|
+
"context": "Use 'kubectl-mcp-server context <name>' to switch context",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return CliError(
|
|
184
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
185
|
+
type="MISSING_ARGUMENT",
|
|
186
|
+
message=f"Missing required argument: {argument}",
|
|
187
|
+
details=f"Command '{command}' requires {argument}",
|
|
188
|
+
suggestion=suggestions.get(command, "Run 'kubectl-mcp-server --help' for usage")
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def unknown_subcommand_error(subcommand: str) -> CliError:
|
|
193
|
+
valid_commands = "serve, tools, resources, prompts, call, grep, info, context, doctor, version, diagnostics"
|
|
194
|
+
|
|
195
|
+
suggestions = {
|
|
196
|
+
"run": "call",
|
|
197
|
+
"exec": "call",
|
|
198
|
+
"execute": "call",
|
|
199
|
+
"invoke": "call",
|
|
200
|
+
"list": "tools",
|
|
201
|
+
"ls": "tools",
|
|
202
|
+
"get": "tools",
|
|
203
|
+
"show": "info",
|
|
204
|
+
"describe": "tools",
|
|
205
|
+
"search": "grep",
|
|
206
|
+
"find": "grep",
|
|
207
|
+
"check": "doctor",
|
|
208
|
+
"status": "info",
|
|
209
|
+
"start": "serve",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
suggested = suggestions.get(subcommand.lower())
|
|
213
|
+
suggestion_text = (
|
|
214
|
+
f"Did you mean 'kubectl-mcp-server {suggested}'?"
|
|
215
|
+
if suggested
|
|
216
|
+
else "Run 'kubectl-mcp-server --help' for available commands"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return CliError(
|
|
220
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
221
|
+
type="UNKNOWN_SUBCOMMAND",
|
|
222
|
+
message=f'Unknown subcommand: "{subcommand}"',
|
|
223
|
+
details=f"Valid commands: {valid_commands}",
|
|
224
|
+
suggestion=suggestion_text
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def config_error(message: str, suggestion: Optional[str] = None) -> CliError:
|
|
229
|
+
return CliError(
|
|
230
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
231
|
+
type="CONFIG_ERROR",
|
|
232
|
+
message=message,
|
|
233
|
+
suggestion=suggestion or "Check your configuration and try again"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def dependency_missing_error(dependency: str, install_cmd: Optional[str] = None) -> CliError:
|
|
238
|
+
return CliError(
|
|
239
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
240
|
+
type="DEPENDENCY_MISSING",
|
|
241
|
+
message=f"Required dependency not found: {dependency}",
|
|
242
|
+
suggestion=f"Install with: {install_cmd}" if install_cmd else f"Please install {dependency}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def network_error(cause: str) -> CliError:
|
|
247
|
+
suggestion = "Check your network connection and try again"
|
|
248
|
+
|
|
249
|
+
if "ECONNREFUSED" in cause:
|
|
250
|
+
suggestion = "Connection refused. Check if the service is running"
|
|
251
|
+
elif "ETIMEDOUT" in cause or "timeout" in cause.lower():
|
|
252
|
+
suggestion = "Connection timed out. Check network connectivity"
|
|
253
|
+
elif "ENOTFOUND" in cause or "DNS" in cause:
|
|
254
|
+
suggestion = "DNS resolution failed. Check the hostname"
|
|
255
|
+
|
|
256
|
+
return CliError(
|
|
257
|
+
code=ErrorCode.NETWORK_ERROR,
|
|
258
|
+
type="NETWORK_ERROR",
|
|
259
|
+
message="Network error occurred",
|
|
260
|
+
details=cause,
|
|
261
|
+
suggestion=suggestion
|
|
262
|
+
)
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Output formatting utilities for kubectl-mcp-server CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
COLORS = {
|
|
10
|
+
"reset": "\x1b[0m",
|
|
11
|
+
"bold": "\x1b[1m",
|
|
12
|
+
"dim": "\x1b[2m",
|
|
13
|
+
"italic": "\x1b[3m",
|
|
14
|
+
"underline": "\x1b[4m",
|
|
15
|
+
# Foreground colors
|
|
16
|
+
"black": "\x1b[30m",
|
|
17
|
+
"red": "\x1b[31m",
|
|
18
|
+
"green": "\x1b[32m",
|
|
19
|
+
"yellow": "\x1b[33m",
|
|
20
|
+
"blue": "\x1b[34m",
|
|
21
|
+
"magenta": "\x1b[35m",
|
|
22
|
+
"cyan": "\x1b[36m",
|
|
23
|
+
"white": "\x1b[37m",
|
|
24
|
+
# Bright colors
|
|
25
|
+
"bright_black": "\x1b[90m",
|
|
26
|
+
"bright_red": "\x1b[91m",
|
|
27
|
+
"bright_green": "\x1b[92m",
|
|
28
|
+
"bright_yellow": "\x1b[93m",
|
|
29
|
+
"bright_blue": "\x1b[94m",
|
|
30
|
+
"bright_magenta": "\x1b[95m",
|
|
31
|
+
"bright_cyan": "\x1b[96m",
|
|
32
|
+
"bright_white": "\x1b[97m",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def should_colorize() -> bool:
|
|
37
|
+
if os.environ.get("NO_COLOR"):
|
|
38
|
+
return False
|
|
39
|
+
return sys.stdout.isatty()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def color(text: str, *codes: str) -> str:
|
|
43
|
+
if not should_colorize():
|
|
44
|
+
return text
|
|
45
|
+
combined = "".join(codes)
|
|
46
|
+
return f"{combined}{text}{COLORS['reset']}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def bold(text: str) -> str:
|
|
50
|
+
return color(text, COLORS["bold"])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def dim(text: str) -> str:
|
|
54
|
+
return color(text, COLORS["dim"])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cyan(text: str) -> str:
|
|
58
|
+
return color(text, COLORS["cyan"])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def green(text: str) -> str:
|
|
62
|
+
return color(text, COLORS["green"])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def yellow(text: str) -> str:
|
|
66
|
+
return color(text, COLORS["yellow"])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def red(text: str) -> str:
|
|
70
|
+
return color(text, COLORS["red"])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def blue(text: str) -> str:
|
|
74
|
+
return color(text, COLORS["blue"])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
TOOL_CATEGORIES = {
|
|
78
|
+
"pods": "Pods",
|
|
79
|
+
"deployments": "Deployments & Workloads",
|
|
80
|
+
"core": "Core Resources",
|
|
81
|
+
"cluster": "Cluster Management",
|
|
82
|
+
"networking": "Networking",
|
|
83
|
+
"storage": "Storage",
|
|
84
|
+
"security": "Security & RBAC",
|
|
85
|
+
"helm": "Helm",
|
|
86
|
+
"operations": "Operations",
|
|
87
|
+
"diagnostics": "Diagnostics",
|
|
88
|
+
"cost": "Cost Optimization",
|
|
89
|
+
"browser": "Browser Automation",
|
|
90
|
+
"ui": "UI Dashboards",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_tools_list(
|
|
95
|
+
tools: List[Dict[str, Any]],
|
|
96
|
+
with_descriptions: bool = False,
|
|
97
|
+
as_json: bool = False
|
|
98
|
+
) -> str:
|
|
99
|
+
if as_json:
|
|
100
|
+
return json.dumps(tools, indent=2)
|
|
101
|
+
|
|
102
|
+
# Group tools by category
|
|
103
|
+
categories: Dict[str, List[Dict]] = {}
|
|
104
|
+
for tool in tools:
|
|
105
|
+
cat = tool.get("category", "other")
|
|
106
|
+
cat_display = TOOL_CATEGORIES.get(cat, cat.title())
|
|
107
|
+
categories.setdefault(cat_display, []).append(tool)
|
|
108
|
+
|
|
109
|
+
lines = []
|
|
110
|
+
for cat, cat_tools in sorted(categories.items()):
|
|
111
|
+
# Category header
|
|
112
|
+
header = f"{cat} ({len(cat_tools)})"
|
|
113
|
+
lines.append(color(header, COLORS["bold"], COLORS["cyan"]))
|
|
114
|
+
|
|
115
|
+
# Tools in category
|
|
116
|
+
for t in sorted(cat_tools, key=lambda x: x.get("name", "")):
|
|
117
|
+
name = t.get("name", "unknown")
|
|
118
|
+
if with_descriptions and t.get("description"):
|
|
119
|
+
desc = t["description"]
|
|
120
|
+
# Truncate long descriptions
|
|
121
|
+
if len(desc) > 60:
|
|
122
|
+
desc = desc[:57] + "..."
|
|
123
|
+
lines.append(f" {green(name)} - {dim(desc)}")
|
|
124
|
+
else:
|
|
125
|
+
lines.append(f" {name}")
|
|
126
|
+
|
|
127
|
+
lines.append("") # Empty line between categories
|
|
128
|
+
|
|
129
|
+
return "\n".join(lines).rstrip()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def format_tool_schema(tool: Dict[str, Any], as_json: bool = False) -> str:
|
|
133
|
+
if as_json:
|
|
134
|
+
return json.dumps(tool, indent=2)
|
|
135
|
+
|
|
136
|
+
lines = []
|
|
137
|
+
name = tool.get("name", "unknown")
|
|
138
|
+
description = tool.get("description", "")
|
|
139
|
+
schema = tool.get("inputSchema", tool.get("input_schema", {}))
|
|
140
|
+
|
|
141
|
+
# Header
|
|
142
|
+
lines.append(f"{bold('Tool:')} {green(name)}")
|
|
143
|
+
if description:
|
|
144
|
+
lines.append(f"{bold('Description:')} {description}")
|
|
145
|
+
|
|
146
|
+
# Parameters
|
|
147
|
+
properties = schema.get("properties", {})
|
|
148
|
+
required = schema.get("required", [])
|
|
149
|
+
|
|
150
|
+
if properties:
|
|
151
|
+
lines.append("")
|
|
152
|
+
lines.append(bold("Parameters:"))
|
|
153
|
+
for param_name, param_info in properties.items():
|
|
154
|
+
param_type = param_info.get("type", "any")
|
|
155
|
+
param_desc = param_info.get("description", "")
|
|
156
|
+
is_required = param_name in required
|
|
157
|
+
req_str = yellow("required") if is_required else dim("optional")
|
|
158
|
+
|
|
159
|
+
param_line = f" {cyan(param_name)} ({param_type}, {req_str})"
|
|
160
|
+
if param_desc:
|
|
161
|
+
lines.append(f"{param_line}")
|
|
162
|
+
lines.append(f" {dim(param_desc)}")
|
|
163
|
+
else:
|
|
164
|
+
lines.append(param_line)
|
|
165
|
+
|
|
166
|
+
# Full schema
|
|
167
|
+
lines.append("")
|
|
168
|
+
lines.append(bold("Input Schema:"))
|
|
169
|
+
lines.append(json.dumps(schema, indent=2))
|
|
170
|
+
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def format_tools_search(
|
|
175
|
+
results: List[Dict[str, Any]],
|
|
176
|
+
pattern: str,
|
|
177
|
+
with_descriptions: bool = False
|
|
178
|
+
) -> str:
|
|
179
|
+
if not results:
|
|
180
|
+
return dim(f"No tools matching '{pattern}'")
|
|
181
|
+
|
|
182
|
+
lines = [f"Found {len(results)} tools matching '{pattern}':", ""]
|
|
183
|
+
|
|
184
|
+
for tool in results:
|
|
185
|
+
name = tool.get("name", "unknown")
|
|
186
|
+
if with_descriptions and tool.get("description"):
|
|
187
|
+
lines.append(f"{green(name)} - {dim(tool['description'])}")
|
|
188
|
+
else:
|
|
189
|
+
lines.append(green(name))
|
|
190
|
+
|
|
191
|
+
return "\n".join(lines)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def format_resources_list(
|
|
195
|
+
resources: List[Dict[str, Any]],
|
|
196
|
+
as_json: bool = False
|
|
197
|
+
) -> str:
|
|
198
|
+
if as_json:
|
|
199
|
+
return json.dumps(resources, indent=2)
|
|
200
|
+
|
|
201
|
+
lines = [bold(f"Resources ({len(resources)}):"), ""]
|
|
202
|
+
|
|
203
|
+
for res in resources:
|
|
204
|
+
uri = res.get("uri", res.get("name", "unknown"))
|
|
205
|
+
name = res.get("name", uri)
|
|
206
|
+
description = res.get("description", "")
|
|
207
|
+
|
|
208
|
+
lines.append(f" {cyan(uri)}")
|
|
209
|
+
if name != uri:
|
|
210
|
+
lines.append(f" Name: {name}")
|
|
211
|
+
if description:
|
|
212
|
+
lines.append(f" {dim(description)}")
|
|
213
|
+
|
|
214
|
+
return "\n".join(lines)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def format_prompts_list(
|
|
218
|
+
prompts: List[Dict[str, Any]],
|
|
219
|
+
as_json: bool = False
|
|
220
|
+
) -> str:
|
|
221
|
+
if as_json:
|
|
222
|
+
return json.dumps(prompts, indent=2)
|
|
223
|
+
|
|
224
|
+
lines = [bold(f"Prompts ({len(prompts)}):"), ""]
|
|
225
|
+
|
|
226
|
+
for prompt in prompts:
|
|
227
|
+
name = prompt.get("name", "unknown")
|
|
228
|
+
description = prompt.get("description", "")
|
|
229
|
+
args = prompt.get("arguments", [])
|
|
230
|
+
|
|
231
|
+
lines.append(f" {green(name)}")
|
|
232
|
+
if description:
|
|
233
|
+
lines.append(f" {dim(description)}")
|
|
234
|
+
if args:
|
|
235
|
+
arg_names = [a.get("name", "?") for a in args]
|
|
236
|
+
lines.append(f" Arguments: {', '.join(arg_names)}")
|
|
237
|
+
|
|
238
|
+
return "\n".join(lines)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def format_call_result(result: Any, as_json: bool = False) -> str:
|
|
242
|
+
if as_json or not sys.stdout.isatty():
|
|
243
|
+
return json.dumps(result, indent=2, default=str)
|
|
244
|
+
|
|
245
|
+
# Try to extract text content from MCP result format
|
|
246
|
+
if isinstance(result, dict):
|
|
247
|
+
content = result.get("content", [])
|
|
248
|
+
if isinstance(content, list):
|
|
249
|
+
text_parts = []
|
|
250
|
+
for item in content:
|
|
251
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
252
|
+
text_parts.append(item.get("text", ""))
|
|
253
|
+
if text_parts:
|
|
254
|
+
return "\n".join(text_parts)
|
|
255
|
+
|
|
256
|
+
if result.get("isError"):
|
|
257
|
+
error_msg = result.get("error", result.get("message", "Unknown error"))
|
|
258
|
+
return red(f"Error: {error_msg}")
|
|
259
|
+
|
|
260
|
+
return json.dumps(result, indent=2, default=str)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def format_server_info(
|
|
264
|
+
version: str,
|
|
265
|
+
tool_count: int,
|
|
266
|
+
resource_count: int,
|
|
267
|
+
prompt_count: int,
|
|
268
|
+
context: Optional[str] = None,
|
|
269
|
+
as_json: bool = False
|
|
270
|
+
) -> str:
|
|
271
|
+
info = {
|
|
272
|
+
"version": version,
|
|
273
|
+
"tools": tool_count,
|
|
274
|
+
"resources": resource_count,
|
|
275
|
+
"prompts": prompt_count,
|
|
276
|
+
"k8s_context": context,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if as_json:
|
|
280
|
+
return json.dumps(info, indent=2)
|
|
281
|
+
|
|
282
|
+
lines = [
|
|
283
|
+
bold("kubectl-mcp-server"),
|
|
284
|
+
"",
|
|
285
|
+
f" {cyan('Version:')} {version}",
|
|
286
|
+
f" {cyan('Tools:')} {tool_count}",
|
|
287
|
+
f" {cyan('Resources:')} {resource_count}",
|
|
288
|
+
f" {cyan('Prompts:')} {prompt_count}",
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
if context:
|
|
292
|
+
lines.append(f" {cyan('K8s Context:')} {context}")
|
|
293
|
+
|
|
294
|
+
return "\n".join(lines)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def format_context_info(
|
|
298
|
+
current: str,
|
|
299
|
+
available: List[str],
|
|
300
|
+
as_json: bool = False
|
|
301
|
+
) -> str:
|
|
302
|
+
if as_json:
|
|
303
|
+
return json.dumps({
|
|
304
|
+
"current": current,
|
|
305
|
+
"available": available
|
|
306
|
+
}, indent=2)
|
|
307
|
+
|
|
308
|
+
lines = [
|
|
309
|
+
f"{bold('Current context:')} {green(current)}",
|
|
310
|
+
"",
|
|
311
|
+
bold("Available contexts:"),
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
for ctx in available:
|
|
315
|
+
if ctx == current:
|
|
316
|
+
lines.append(f" {green('*')} {green(ctx)} (current)")
|
|
317
|
+
else:
|
|
318
|
+
lines.append(f" {ctx}")
|
|
319
|
+
|
|
320
|
+
return "\n".join(lines)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def format_doctor_results(
|
|
324
|
+
checks: List[Dict[str, Any]],
|
|
325
|
+
as_json: bool = False
|
|
326
|
+
) -> str:
|
|
327
|
+
if as_json:
|
|
328
|
+
return json.dumps(checks, indent=2)
|
|
329
|
+
|
|
330
|
+
lines = [bold("Checking dependencies..."), ""]
|
|
331
|
+
all_passed = True
|
|
332
|
+
|
|
333
|
+
for check in checks:
|
|
334
|
+
name = check.get("name", "unknown")
|
|
335
|
+
status = check.get("status", "unknown")
|
|
336
|
+
details = check.get("details", "")
|
|
337
|
+
version = check.get("version", "")
|
|
338
|
+
|
|
339
|
+
if status == "ok":
|
|
340
|
+
icon = green("✓")
|
|
341
|
+
status_text = green("OK")
|
|
342
|
+
elif status == "warning":
|
|
343
|
+
icon = yellow("!")
|
|
344
|
+
status_text = yellow("WARNING")
|
|
345
|
+
all_passed = False
|
|
346
|
+
else:
|
|
347
|
+
icon = red("✗")
|
|
348
|
+
status_text = red("FAILED")
|
|
349
|
+
all_passed = False
|
|
350
|
+
|
|
351
|
+
line = f" {icon} {name}: {status_text}"
|
|
352
|
+
if version:
|
|
353
|
+
line += f" ({version})"
|
|
354
|
+
lines.append(line)
|
|
355
|
+
|
|
356
|
+
if details and status != "ok":
|
|
357
|
+
lines.append(f" {dim(details)}")
|
|
358
|
+
|
|
359
|
+
lines.append("")
|
|
360
|
+
if all_passed:
|
|
361
|
+
lines.append(green("All checks passed!"))
|
|
362
|
+
else:
|
|
363
|
+
lines.append(yellow("Some checks failed. See details above."))
|
|
364
|
+
|
|
365
|
+
return "\n".join(lines)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def format_error(message: str) -> str:
|
|
369
|
+
return red(f"Error: {message}")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def format_warning(message: str) -> str:
|
|
373
|
+
return yellow(f"Warning: {message}")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def format_success(message: str) -> str:
|
|
377
|
+
return green(message)
|