kubectl-mcp-server 1.16.0__py3-none-any.whl → 1.18.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.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/METADATA +48 -1
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/RECORD +30 -15
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/k8s_config.py +127 -1
- kubectl_mcp_tool/mcp_server.py +219 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/providers.py +347 -0
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- tests/test_config.py +386 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.16.0.dist-info → kubectl_mcp_server-1.18.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom prompt loading and rendering system for kubectl-mcp-server.
|
|
3
|
+
|
|
4
|
+
Supports user-defined workflow prompts via TOML configuration file with
|
|
5
|
+
Mustache-style template syntax ({{variable}}) and conditional sections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import logging
|
|
10
|
+
from typing import List, Dict, Any, Optional
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("mcp-server")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PromptArgument:
|
|
18
|
+
"""Definition of a prompt argument."""
|
|
19
|
+
name: str
|
|
20
|
+
description: str = ""
|
|
21
|
+
required: bool = False
|
|
22
|
+
default: str = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PromptMessage:
|
|
27
|
+
"""A single message in a prompt conversation."""
|
|
28
|
+
role: str # "user" or "assistant"
|
|
29
|
+
content: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CustomPrompt:
|
|
34
|
+
"""A custom prompt definition."""
|
|
35
|
+
name: str
|
|
36
|
+
description: str
|
|
37
|
+
title: str = ""
|
|
38
|
+
arguments: List[PromptArgument] = field(default_factory=list)
|
|
39
|
+
messages: List[PromptMessage] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_prompt(prompt: CustomPrompt, args: Dict[str, str]) -> List[PromptMessage]:
|
|
43
|
+
"""
|
|
44
|
+
Render prompt messages with argument substitution using {{arg_name}} syntax.
|
|
45
|
+
|
|
46
|
+
Supports:
|
|
47
|
+
- Simple substitution: {{variable}} -> value
|
|
48
|
+
- Conditional sections: {{#variable}}content{{/variable}} (shown if variable is truthy)
|
|
49
|
+
- Inverse sections: {{^variable}}content{{/variable}} (shown if variable is falsy)
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
prompt: The CustomPrompt to render
|
|
53
|
+
args: Dictionary of argument names to values
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of PromptMessage with rendered content
|
|
57
|
+
"""
|
|
58
|
+
rendered = []
|
|
59
|
+
for msg in prompt.messages:
|
|
60
|
+
content = msg.content
|
|
61
|
+
|
|
62
|
+
# Process conditional sections ({{#var}}...{{/var}})
|
|
63
|
+
# These are shown only if the variable exists and is truthy
|
|
64
|
+
def process_conditional(match):
|
|
65
|
+
var_name = match.group(1)
|
|
66
|
+
section_content = match.group(2)
|
|
67
|
+
var_value = args.get(var_name, "")
|
|
68
|
+
# Only render section if variable exists and is not empty/false
|
|
69
|
+
if var_value and var_value.lower() not in ("false", "0", "no"):
|
|
70
|
+
# Recursively process the section content for variable substitution
|
|
71
|
+
processed = section_content
|
|
72
|
+
for key, value in args.items():
|
|
73
|
+
processed = processed.replace(f"{{{{{key}}}}}", str(value))
|
|
74
|
+
return processed
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
content = re.sub(
|
|
78
|
+
r'\{\{#(\w+)\}\}(.*?)\{\{/\1\}\}',
|
|
79
|
+
process_conditional,
|
|
80
|
+
content,
|
|
81
|
+
flags=re.DOTALL
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Process inverse sections ({{^var}}...{{/var}})
|
|
85
|
+
# These are shown only if the variable is missing or falsy
|
|
86
|
+
def process_inverse(match):
|
|
87
|
+
var_name = match.group(1)
|
|
88
|
+
section_content = match.group(2)
|
|
89
|
+
var_value = args.get(var_name, "")
|
|
90
|
+
# Only render section if variable is missing or falsy
|
|
91
|
+
if not var_value or var_value.lower() in ("false", "0", "no"):
|
|
92
|
+
processed = section_content
|
|
93
|
+
for key, value in args.items():
|
|
94
|
+
processed = processed.replace(f"{{{{{key}}}}}", str(value))
|
|
95
|
+
return processed
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
content = re.sub(
|
|
99
|
+
r'\{\{\^(\w+)\}\}(.*?)\{\{/\1\}\}',
|
|
100
|
+
process_inverse,
|
|
101
|
+
content,
|
|
102
|
+
flags=re.DOTALL
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Simple variable substitution
|
|
106
|
+
for key, value in args.items():
|
|
107
|
+
content = content.replace(f"{{{{{key}}}}}", str(value))
|
|
108
|
+
|
|
109
|
+
# Remove unsubstituted optional placeholders (simple variables only)
|
|
110
|
+
content = re.sub(r'\{\{[^#^/][^}]*\}\}', '', content)
|
|
111
|
+
|
|
112
|
+
# Clean up any remaining empty lines from removed sections
|
|
113
|
+
content = re.sub(r'\n\s*\n\s*\n', '\n\n', content)
|
|
114
|
+
|
|
115
|
+
rendered.append(PromptMessage(role=msg.role, content=content.strip()))
|
|
116
|
+
|
|
117
|
+
return rendered
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_prompts_from_config(config: dict) -> List[CustomPrompt]:
|
|
121
|
+
"""
|
|
122
|
+
Load prompts from config dict (from TOML).
|
|
123
|
+
|
|
124
|
+
Expected config structure:
|
|
125
|
+
{
|
|
126
|
+
"prompts": [
|
|
127
|
+
{
|
|
128
|
+
"name": "debug-pod",
|
|
129
|
+
"title": "Debug Pod Issues",
|
|
130
|
+
"description": "Diagnose pod problems",
|
|
131
|
+
"arguments": [
|
|
132
|
+
{"name": "pod_name", "required": True, "description": "Pod to debug"},
|
|
133
|
+
{"name": "namespace", "required": False, "default": "default"}
|
|
134
|
+
],
|
|
135
|
+
"messages": [
|
|
136
|
+
{"role": "user", "content": "Debug pod {{pod_name}} in namespace {{namespace}}"}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
config: Dictionary loaded from TOML configuration
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of CustomPrompt objects
|
|
147
|
+
"""
|
|
148
|
+
prompts = []
|
|
149
|
+
|
|
150
|
+
prompt_configs = config.get("prompts", [])
|
|
151
|
+
if not prompt_configs:
|
|
152
|
+
return prompts
|
|
153
|
+
|
|
154
|
+
for prompt_config in prompt_configs:
|
|
155
|
+
try:
|
|
156
|
+
# Parse arguments
|
|
157
|
+
arguments = []
|
|
158
|
+
for arg_config in prompt_config.get("arguments", []):
|
|
159
|
+
arg = PromptArgument(
|
|
160
|
+
name=arg_config.get("name", ""),
|
|
161
|
+
description=arg_config.get("description", ""),
|
|
162
|
+
required=arg_config.get("required", False),
|
|
163
|
+
default=arg_config.get("default", "")
|
|
164
|
+
)
|
|
165
|
+
if arg.name: # Only add valid arguments
|
|
166
|
+
arguments.append(arg)
|
|
167
|
+
|
|
168
|
+
# Parse messages
|
|
169
|
+
messages = []
|
|
170
|
+
for msg_config in prompt_config.get("messages", []):
|
|
171
|
+
msg = PromptMessage(
|
|
172
|
+
role=msg_config.get("role", "user"),
|
|
173
|
+
content=msg_config.get("content", "")
|
|
174
|
+
)
|
|
175
|
+
if msg.content: # Only add non-empty messages
|
|
176
|
+
messages.append(msg)
|
|
177
|
+
|
|
178
|
+
# Create prompt
|
|
179
|
+
prompt = CustomPrompt(
|
|
180
|
+
name=prompt_config.get("name", ""),
|
|
181
|
+
title=prompt_config.get("title", prompt_config.get("name", "")),
|
|
182
|
+
description=prompt_config.get("description", ""),
|
|
183
|
+
arguments=arguments,
|
|
184
|
+
messages=messages
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if prompt.name: # Only add valid prompts
|
|
188
|
+
prompts.append(prompt)
|
|
189
|
+
logger.debug(f"Loaded custom prompt: {prompt.name}")
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(f"Failed to parse prompt config: {e}")
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
return prompts
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def load_prompts_from_toml_file(file_path: str) -> List[CustomPrompt]:
|
|
199
|
+
"""
|
|
200
|
+
Load prompts from a TOML file.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
file_path: Path to the TOML configuration file
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of CustomPrompt objects
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
import tomllib
|
|
210
|
+
except ImportError:
|
|
211
|
+
try:
|
|
212
|
+
import tomli as tomllib
|
|
213
|
+
except ImportError:
|
|
214
|
+
logger.warning("TOML parsing not available. Install tomli for Python < 3.11")
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
with open(file_path, "rb") as f:
|
|
219
|
+
config = tomllib.load(f)
|
|
220
|
+
return load_prompts_from_config(config)
|
|
221
|
+
except FileNotFoundError:
|
|
222
|
+
logger.debug(f"Custom prompts file not found: {file_path}")
|
|
223
|
+
return []
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.warning(f"Failed to load prompts from {file_path}: {e}")
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def validate_prompt_args(prompt: CustomPrompt, args: Dict[str, str]) -> List[str]:
|
|
230
|
+
"""
|
|
231
|
+
Validate that all required arguments are provided.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
prompt: The CustomPrompt to validate against
|
|
235
|
+
args: Dictionary of argument names to values
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of error messages (empty if valid)
|
|
239
|
+
"""
|
|
240
|
+
errors = []
|
|
241
|
+
|
|
242
|
+
for arg in prompt.arguments:
|
|
243
|
+
if arg.required and arg.name not in args:
|
|
244
|
+
errors.append(f"Missing required argument: {arg.name}")
|
|
245
|
+
elif arg.required and not args.get(arg.name):
|
|
246
|
+
errors.append(f"Required argument cannot be empty: {arg.name}")
|
|
247
|
+
|
|
248
|
+
return errors
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def apply_defaults(prompt: CustomPrompt, args: Dict[str, str]) -> Dict[str, str]:
|
|
252
|
+
"""
|
|
253
|
+
Apply default values for missing optional arguments.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
prompt: The CustomPrompt with argument definitions
|
|
257
|
+
args: Dictionary of argument names to values
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
New dictionary with defaults applied
|
|
261
|
+
"""
|
|
262
|
+
result = dict(args)
|
|
263
|
+
|
|
264
|
+
for arg in prompt.arguments:
|
|
265
|
+
if arg.name not in result and arg.default:
|
|
266
|
+
result[arg.name] = arg.default
|
|
267
|
+
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_prompt_schema(prompt: CustomPrompt) -> Dict[str, Any]:
|
|
272
|
+
"""
|
|
273
|
+
Generate JSON Schema for prompt arguments.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
prompt: The CustomPrompt to generate schema for
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
JSON Schema dictionary
|
|
280
|
+
"""
|
|
281
|
+
properties = {}
|
|
282
|
+
required = []
|
|
283
|
+
|
|
284
|
+
for arg in prompt.arguments:
|
|
285
|
+
properties[arg.name] = {
|
|
286
|
+
"type": "string",
|
|
287
|
+
"description": arg.description or f"Argument: {arg.name}"
|
|
288
|
+
}
|
|
289
|
+
if arg.default:
|
|
290
|
+
properties[arg.name]["default"] = arg.default
|
|
291
|
+
if arg.required:
|
|
292
|
+
required.append(arg.name)
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"type": "object",
|
|
296
|
+
"properties": properties,
|
|
297
|
+
"required": required
|
|
298
|
+
}
|
|
@@ -1,14 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP prompts registration for kubectl-mcp-server.
|
|
3
|
+
|
|
4
|
+
This module handles registration of both built-in and custom prompts.
|
|
5
|
+
Custom prompts can be loaded from a TOML configuration file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import logging
|
|
2
|
-
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
from .custom import (
|
|
13
|
+
CustomPrompt,
|
|
14
|
+
PromptMessage,
|
|
15
|
+
render_prompt,
|
|
16
|
+
load_prompts_from_toml_file,
|
|
17
|
+
validate_prompt_args,
|
|
18
|
+
apply_defaults,
|
|
19
|
+
)
|
|
20
|
+
from .builtin import get_builtin_prompts
|
|
3
21
|
|
|
4
22
|
logger = logging.getLogger("mcp-server")
|
|
5
23
|
|
|
6
24
|
|
|
7
|
-
|
|
8
|
-
|
|
25
|
+
# Default paths for custom prompts configuration
|
|
26
|
+
DEFAULT_CONFIG_PATHS = [
|
|
27
|
+
os.path.expanduser("~/.kubectl-mcp/prompts.toml"),
|
|
28
|
+
os.path.expanduser("~/.config/kubectl-mcp/prompts.toml"),
|
|
29
|
+
"./kubectl-mcp-prompts.toml",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_custom_prompts_path() -> Optional[str]:
|
|
34
|
+
"""
|
|
35
|
+
Get the path to custom prompts configuration file.
|
|
36
|
+
|
|
37
|
+
Checks (in order):
|
|
38
|
+
1. MCP_PROMPTS_FILE environment variable
|
|
39
|
+
2. Default config paths
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Path to config file if found, None otherwise
|
|
43
|
+
"""
|
|
44
|
+
# Check environment variable first
|
|
45
|
+
env_path = os.environ.get("MCP_PROMPTS_FILE")
|
|
46
|
+
if env_path and os.path.isfile(env_path):
|
|
47
|
+
return env_path
|
|
48
|
+
|
|
49
|
+
# Check default paths
|
|
50
|
+
for path in DEFAULT_CONFIG_PATHS:
|
|
51
|
+
if os.path.isfile(path):
|
|
52
|
+
return path
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _merge_prompts(builtin: list, custom: list) -> Dict[str, CustomPrompt]:
|
|
58
|
+
"""
|
|
59
|
+
Merge built-in and custom prompts.
|
|
60
|
+
|
|
61
|
+
Custom prompts override built-in prompts with the same name.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
builtin: List of built-in CustomPrompt objects
|
|
65
|
+
custom: List of custom CustomPrompt objects
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary of prompt name -> CustomPrompt
|
|
69
|
+
"""
|
|
70
|
+
prompts = {}
|
|
71
|
+
|
|
72
|
+
# Add built-in prompts first
|
|
73
|
+
for prompt in builtin:
|
|
74
|
+
prompts[prompt.name] = prompt
|
|
75
|
+
|
|
76
|
+
# Custom prompts override built-in
|
|
77
|
+
for prompt in custom:
|
|
78
|
+
if prompt.name in prompts:
|
|
79
|
+
logger.info(f"Custom prompt '{prompt.name}' overrides built-in prompt")
|
|
80
|
+
prompts[prompt.name] = prompt
|
|
81
|
+
|
|
82
|
+
return prompts
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def register_prompts(server, config_path: Optional[str] = None):
|
|
86
|
+
"""
|
|
87
|
+
Register all MCP prompts for Kubernetes workflows.
|
|
88
|
+
|
|
89
|
+
Registers:
|
|
90
|
+
1. Built-in prompts from builtin.py
|
|
91
|
+
2. Custom prompts from configuration file (if found)
|
|
92
|
+
3. Original inline prompts for backward compatibility
|
|
93
|
+
|
|
94
|
+
Custom prompts can override built-in prompts by using the same name.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
server: FastMCP server instance
|
|
98
|
+
config_path: Optional path to custom prompts TOML file
|
|
99
|
+
"""
|
|
100
|
+
# Load built-in prompts
|
|
101
|
+
builtin_prompts = get_builtin_prompts()
|
|
102
|
+
logger.debug(f"Loaded {len(builtin_prompts)} built-in prompts")
|
|
103
|
+
|
|
104
|
+
# Load custom prompts
|
|
105
|
+
prompts_file = config_path or _get_custom_prompts_path()
|
|
106
|
+
custom_prompts = []
|
|
107
|
+
if prompts_file:
|
|
108
|
+
custom_prompts = load_prompts_from_toml_file(prompts_file)
|
|
109
|
+
logger.info(f"Loaded {len(custom_prompts)} custom prompts from {prompts_file}")
|
|
110
|
+
|
|
111
|
+
# Merge prompts (custom overrides built-in)
|
|
112
|
+
all_prompts = _merge_prompts(builtin_prompts, custom_prompts)
|
|
113
|
+
|
|
114
|
+
# Register each configurable prompt
|
|
115
|
+
for prompt in all_prompts.values():
|
|
116
|
+
_register_custom_prompt(server, prompt)
|
|
117
|
+
|
|
118
|
+
logger.debug(f"Registered {len(all_prompts)} configurable prompts")
|
|
119
|
+
|
|
120
|
+
# Register original inline prompts for backward compatibility
|
|
121
|
+
_register_inline_prompts(server)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _register_custom_prompt(server, prompt: CustomPrompt):
|
|
125
|
+
"""
|
|
126
|
+
Register a single CustomPrompt with the server.
|
|
9
127
|
|
|
10
128
|
Args:
|
|
11
129
|
server: FastMCP server instance
|
|
130
|
+
prompt: CustomPrompt to register
|
|
131
|
+
"""
|
|
132
|
+
# Build the argument schema for FastMCP
|
|
133
|
+
def create_prompt_handler(p: CustomPrompt):
|
|
134
|
+
"""Create a closure that captures the prompt."""
|
|
135
|
+
def handler(**kwargs) -> str:
|
|
136
|
+
# Apply defaults for missing optional arguments
|
|
137
|
+
args = apply_defaults(p, kwargs)
|
|
138
|
+
|
|
139
|
+
# Validate required arguments
|
|
140
|
+
errors = validate_prompt_args(p, args)
|
|
141
|
+
if errors:
|
|
142
|
+
return f"Error: {'; '.join(errors)}"
|
|
143
|
+
|
|
144
|
+
# Render the prompt messages
|
|
145
|
+
rendered = render_prompt(p, args)
|
|
146
|
+
|
|
147
|
+
# Return the content (for now, just the first message)
|
|
148
|
+
# MCP prompts typically return a single string
|
|
149
|
+
if rendered:
|
|
150
|
+
return rendered[0].content
|
|
151
|
+
return f"Prompt '{p.name}' has no messages defined."
|
|
152
|
+
|
|
153
|
+
return handler
|
|
154
|
+
|
|
155
|
+
# Create the handler
|
|
156
|
+
handler = create_prompt_handler(prompt)
|
|
157
|
+
|
|
158
|
+
# Set function metadata for FastMCP registration
|
|
159
|
+
handler.__name__ = prompt.name.replace("-", "_")
|
|
160
|
+
handler.__doc__ = prompt.description or prompt.title
|
|
161
|
+
|
|
162
|
+
# Build parameter annotations from arguments
|
|
163
|
+
params = {}
|
|
164
|
+
for arg in prompt.arguments:
|
|
165
|
+
# All prompt arguments are strings with Optional if not required
|
|
166
|
+
if arg.required:
|
|
167
|
+
params[arg.name] = str
|
|
168
|
+
else:
|
|
169
|
+
params[arg.name] = Optional[str]
|
|
170
|
+
|
|
171
|
+
handler.__annotations__ = params
|
|
172
|
+
handler.__annotations__["return"] = str
|
|
173
|
+
|
|
174
|
+
# Register with server
|
|
175
|
+
try:
|
|
176
|
+
server.prompt()(handler)
|
|
177
|
+
logger.debug(f"Registered configurable prompt: {prompt.name}")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.warning(f"Failed to register prompt '{prompt.name}': {e}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _register_inline_prompts(server):
|
|
183
|
+
"""
|
|
184
|
+
Register original inline prompts for backward compatibility.
|
|
185
|
+
|
|
186
|
+
These prompts are kept for users who may be using them directly.
|
|
187
|
+
They can be overridden by custom prompts with the same name.
|
|
12
188
|
"""
|
|
13
189
|
|
|
14
190
|
@server.prompt()
|
|
@@ -605,7 +781,7 @@ Target Replicas: {target_replicas}
|
|
|
605
781
|
|
|
606
782
|
### Step 2: Capacity Planning
|
|
607
783
|
Calculate required resources:
|
|
608
|
-
- Current pod resources
|
|
784
|
+
- Current pod resources x {target_replicas} = Total needed
|
|
609
785
|
- Check node capacity: `kubectl_top("nodes")`
|
|
610
786
|
- Verify cluster can accommodate new pods
|
|
611
787
|
|