glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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.
Files changed (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -1,1187 +0,0 @@
1
- """MCP management commands.
2
-
3
- Authors:
4
- Raymond Christopher (raymond.christopher@gdplabs.id)
5
- Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
6
- """
7
-
8
- import json
9
- import sys
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- import click
14
- from rich.console import Console
15
-
16
- from glaip_sdk.branding import (
17
- ACCENT_STYLE,
18
- INFO,
19
- SUCCESS,
20
- SUCCESS_STYLE,
21
- WARNING_STYLE,
22
- )
23
- from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
24
- from glaip_sdk.cli.display import (
25
- display_api_error,
26
- display_confirmation_prompt,
27
- display_creation_success,
28
- display_deletion_success,
29
- display_update_success,
30
- handle_json_output,
31
- handle_rich_output,
32
- )
33
- from glaip_sdk.cli.io import (
34
- fetch_raw_resource_details,
35
- load_resource_from_file_with_validation,
36
- )
37
- from glaip_sdk.cli.mcp_validators import (
38
- validate_mcp_auth_structure,
39
- validate_mcp_config_structure,
40
- )
41
- from glaip_sdk.cli.parsers.json_input import parse_json_input
42
- from glaip_sdk.cli.resolution import resolve_resource_reference
43
- from glaip_sdk.cli.rich_helpers import print_markup
44
- from glaip_sdk.cli.utils import (
45
- coerce_to_row,
46
- get_client,
47
- output_list,
48
- output_result,
49
- spinner_context,
50
- )
51
- from glaip_sdk.config.constants import (
52
- DEFAULT_MCP_TYPE,
53
- )
54
- from glaip_sdk.icons import ICON_TOOL
55
- from glaip_sdk.rich_components import AIPPanel
56
- from glaip_sdk.utils import format_datetime
57
- from glaip_sdk.utils.import_export import convert_export_to_import_format
58
- from glaip_sdk.utils.serialization import (
59
- build_mcp_export_payload,
60
- write_resource_export,
61
- )
62
-
63
- console = Console()
64
-
65
-
66
- def _is_sensitive_data(val: Any) -> bool:
67
- """Check if value contains sensitive authentication data.
68
-
69
- Args:
70
- val: Value to check for sensitive information
71
-
72
- Returns:
73
- True if the value appears to contain sensitive data
74
- """
75
- if not isinstance(val, dict):
76
- return False
77
-
78
- sensitive_patterns = {"token", "password", "secret", "key", "credential"}
79
- return any(pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns)
80
-
81
-
82
- def _redact_sensitive_dict(val: dict[str, Any]) -> dict[str, Any]:
83
- """Redact sensitive fields from a dictionary.
84
-
85
- Args:
86
- val: Dictionary to redact
87
-
88
- Returns:
89
- Redacted dictionary
90
- """
91
- redacted = val.copy()
92
- sensitive_patterns = {"token", "password", "secret", "key", "credential"}
93
- for k in redacted.keys():
94
- if any(pattern in k.lower() for pattern in sensitive_patterns):
95
- redacted[k] = "<REDACTED>"
96
- return redacted
97
-
98
-
99
- def _format_dict_value(val: dict[str, Any]) -> str:
100
- """Format a dictionary value for display.
101
-
102
- Args:
103
- val: Dictionary to format
104
-
105
- Returns:
106
- Formatted string representation
107
- """
108
- if _is_sensitive_data(val):
109
- redacted = _redact_sensitive_dict(val)
110
- return json.dumps(redacted, indent=2)
111
- return json.dumps(val, indent=2)
112
-
113
-
114
- def _format_preview_value(val: Any) -> str:
115
- """Format a value for display in update preview with sensitive data redaction.
116
-
117
- Args:
118
- val: Value to format
119
-
120
- Returns:
121
- Formatted string representation of the value
122
- """
123
- if val is None:
124
- return "[dim]None[/dim]"
125
- if isinstance(val, dict):
126
- return _format_dict_value(val)
127
- if isinstance(val, str):
128
- return f'"{val}"' if val else '""'
129
- return str(val)
130
-
131
-
132
- def _build_empty_override_warnings(empty_fields: list[str]) -> list[str]:
133
- """Build warning lines for empty CLI overrides.
134
-
135
- Args:
136
- empty_fields: List of field names with empty string overrides
137
-
138
- Returns:
139
- List of formatted warning lines
140
- """
141
- if not empty_fields:
142
- return []
143
-
144
- warnings = ["\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"]
145
- warnings.extend(f"- [yellow]{field}: will be set to empty string[/yellow]" for field in empty_fields)
146
- return warnings
147
-
148
-
149
- def _validate_import_payload_fields(import_payload: dict[str, Any]) -> bool:
150
- """Validate that import payload contains updatable fields.
151
-
152
- Args:
153
- import_payload: Import payload to validate
154
-
155
- Returns:
156
- True if payload has updatable fields, False otherwise
157
- """
158
- updatable_fields = {"name", "transport", "description", "config", "authentication"}
159
- has_updatable = any(field in import_payload for field in updatable_fields)
160
-
161
- if not has_updatable:
162
- available_fields = set(import_payload.keys())
163
- print_markup(
164
- "[yellow]⚠️ No updatable fields found in import file.[/yellow]\n"
165
- f"[dim]Found fields: {', '.join(sorted(available_fields))}[/dim]\n"
166
- f"[dim]Updatable fields: {', '.join(sorted(updatable_fields))}[/dim]"
167
- )
168
- return has_updatable
169
-
170
-
171
- def _get_config_transport(
172
- transport: str | None,
173
- import_payload: dict[str, Any] | None,
174
- mcp: Any,
175
- ) -> str | None:
176
- """Get the transport value for config validation.
177
-
178
- Args:
179
- transport: CLI transport flag
180
- import_payload: Optional import payload
181
- mcp: Current MCP object
182
-
183
- Returns:
184
- Transport value or None
185
- """
186
- if import_payload:
187
- return transport or import_payload.get("transport")
188
- return transport or getattr(mcp, "transport", None)
189
-
190
-
191
- def _build_update_data_from_sources(
192
- import_payload: dict[str, Any] | None,
193
- mcp: Any,
194
- name: str | None,
195
- transport: str | None,
196
- description: str | None,
197
- config: str | None,
198
- auth: str | None,
199
- ) -> dict[str, Any]:
200
- """Build update data from import payload and CLI flags.
201
-
202
- Args:
203
- import_payload: Optional import payload
204
- mcp: Current MCP object
205
- name: CLI name flag
206
- transport: CLI transport flag
207
- description: CLI description flag
208
- config: CLI config flag
209
- auth: CLI auth flag
210
-
211
- Returns:
212
- Dictionary with update data
213
- """
214
- update_data = {}
215
-
216
- # Start with import data if available
217
- if import_payload:
218
- updatable_fields = [
219
- "name",
220
- "transport",
221
- "description",
222
- "config",
223
- "authentication",
224
- ]
225
- for field in updatable_fields:
226
- if field in import_payload:
227
- update_data[field] = import_payload[field]
228
-
229
- # CLI flags override import values
230
- if name is not None:
231
- update_data["name"] = name
232
- if transport is not None:
233
- update_data["transport"] = transport
234
- if description is not None:
235
- update_data["description"] = description
236
- if config is not None:
237
- parsed_config = parse_json_input(config)
238
- config_transport = _get_config_transport(transport, import_payload, mcp)
239
- update_data["config"] = validate_mcp_config_structure(
240
- parsed_config,
241
- transport=config_transport,
242
- source="--config",
243
- )
244
- if auth is not None:
245
- parsed_auth = parse_json_input(auth)
246
- update_data["authentication"] = validate_mcp_auth_structure(parsed_auth, source="--auth")
247
-
248
- return update_data
249
-
250
-
251
- def _collect_cli_overrides(
252
- name: str | None,
253
- transport: str | None,
254
- description: str | None,
255
- config: str | None,
256
- auth: str | None,
257
- ) -> dict[str, Any]:
258
- """Collect CLI flags that were explicitly provided.
259
-
260
- Args:
261
- name: CLI name flag
262
- transport: CLI transport flag
263
- description: CLI description flag
264
- config: CLI config flag
265
- auth: CLI auth flag
266
-
267
- Returns:
268
- Dictionary of provided CLI overrides
269
- """
270
- cli_overrides = {}
271
- if name is not None:
272
- cli_overrides["name"] = name
273
- if transport is not None:
274
- cli_overrides["transport"] = transport
275
- if description is not None:
276
- cli_overrides["description"] = description
277
- if config is not None:
278
- cli_overrides["config"] = config
279
- if auth is not None:
280
- cli_overrides["auth"] = auth
281
- return cli_overrides
282
-
283
-
284
- def _handle_cli_error(ctx: Any, error: Exception, operation: str) -> None:
285
- """Render CLI error once and exit with non-zero status."""
286
- handle_json_output(ctx, error=error)
287
- if get_ctx_value(ctx, "view") != "json":
288
- display_api_error(error, operation)
289
- ctx.exit(1)
290
-
291
-
292
- @click.group(name="mcps", no_args_is_help=True)
293
- def mcps_group() -> None:
294
- """MCP management operations.
295
-
296
- Provides commands for creating, listing, updating, deleting, and managing
297
- Model Context Protocol (MCP) configurations.
298
- """
299
- pass
300
-
301
-
302
- def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
303
- """Resolve MCP reference (ID or name) with ambiguity handling.
304
-
305
- Args:
306
- ctx: Click context object
307
- client: API client instance
308
- ref: MCP reference (ID or name)
309
- select: Index to select when multiple matches found
310
-
311
- Returns:
312
- MCP object if found, None otherwise
313
-
314
- Raises:
315
- ClickException: If MCP not found or selection invalid
316
- """
317
- return resolve_resource_reference(
318
- ctx,
319
- client,
320
- ref,
321
- "mcp",
322
- client.mcps.get_mcp_by_id,
323
- client.mcps.find_mcps,
324
- "MCP",
325
- select=select,
326
- )
327
-
328
-
329
- def _strip_server_only_fields(import_data: dict[str, Any]) -> dict[str, Any]:
330
- """Remove fields that should not be forwarded during import-driven creation.
331
-
332
- Args:
333
- import_data: Raw import payload loaded from disk.
334
-
335
- Returns:
336
- A shallow copy of the data with server-managed fields removed.
337
- """
338
- cleaned = dict(import_data)
339
- for key in (
340
- "id",
341
- "type",
342
- "status",
343
- "connection_status",
344
- "created_at",
345
- "updated_at",
346
- ):
347
- cleaned.pop(key, None)
348
- return cleaned
349
-
350
-
351
- def _load_import_ready_payload(import_file: str) -> dict[str, Any]:
352
- """Load and normalise an imported MCP definition for create operations.
353
-
354
- Args:
355
- import_file: Path to an MCP export file (JSON or YAML).
356
-
357
- Returns:
358
- Normalised import payload ready for CLI/REST usage.
359
-
360
- Raises:
361
- click.ClickException: If the file cannot be parsed or validated.
362
- """
363
- raw_data = load_resource_from_file_with_validation(Path(import_file), "MCP")
364
- import_data = convert_export_to_import_format(raw_data)
365
- import_data = _strip_server_only_fields(import_data)
366
-
367
- transport = import_data.get("transport")
368
-
369
- if "config" in import_data:
370
- import_data["config"] = validate_mcp_config_structure(
371
- import_data["config"],
372
- transport=transport,
373
- source="import file",
374
- )
375
-
376
- if "authentication" in import_data:
377
- import_data["authentication"] = validate_mcp_auth_structure(
378
- import_data["authentication"],
379
- source="import file",
380
- )
381
-
382
- return import_data
383
-
384
-
385
- def _coerce_cli_string(value: str | None) -> str | None:
386
- """Normalise CLI string values so blanks are treated as missing.
387
-
388
- Args:
389
- value: User-provided string option.
390
-
391
- Returns:
392
- The stripped string, or ``None`` when the value is blank/whitespace-only.
393
- """
394
- if value is None:
395
- return None
396
- trimmed = value.strip()
397
- # Treat whitespace-only strings as None
398
- return trimmed if trimmed else None
399
-
400
-
401
- def _merge_config_field(
402
- merged_base: dict[str, Any],
403
- cli_config: str | None,
404
- final_transport: str | None,
405
- ) -> None:
406
- """Merge config field with validation.
407
-
408
- Args:
409
- merged_base: Base payload to update in-place.
410
- cli_config: Raw CLI JSON string for config.
411
- final_transport: Transport type for validation.
412
-
413
- Raises:
414
- click.ClickException: If config JSON parsing or validation fails.
415
- """
416
- if cli_config is not None:
417
- parsed_config = parse_json_input(cli_config)
418
- merged_base["config"] = validate_mcp_config_structure(
419
- parsed_config,
420
- transport=final_transport,
421
- source="--config",
422
- )
423
- elif "config" not in merged_base or merged_base["config"] is None:
424
- merged_base["config"] = {}
425
-
426
-
427
- def _merge_auth_field(
428
- merged_base: dict[str, Any],
429
- cli_auth: str | None,
430
- ) -> None:
431
- """Merge authentication field with validation.
432
-
433
- Args:
434
- merged_base: Base payload to update in-place.
435
- cli_auth: Raw CLI JSON string for authentication.
436
-
437
- Raises:
438
- click.ClickException: If auth JSON parsing or validation fails.
439
- """
440
- if cli_auth is not None:
441
- parsed_auth = parse_json_input(cli_auth)
442
- merged_base["authentication"] = validate_mcp_auth_structure(
443
- parsed_auth,
444
- source="--auth",
445
- )
446
- elif "authentication" not in merged_base:
447
- merged_base["authentication"] = None
448
-
449
-
450
- def _merge_import_payload(
451
- import_data: dict[str, Any] | None,
452
- *,
453
- cli_name: str | None,
454
- cli_transport: str | None,
455
- cli_description: str | None,
456
- cli_config: str | None,
457
- cli_auth: str | None,
458
- ) -> tuple[dict[str, Any], list[str]]:
459
- """Merge import data with CLI overrides while tracking missing fields.
460
-
461
- Args:
462
- import_data: Normalised payload loaded from file (if provided).
463
- cli_name: Name supplied via CLI option.
464
- cli_transport: Transport supplied via CLI option.
465
- cli_description: Description supplied via CLI option.
466
- cli_config: Raw CLI JSON string for config.
467
- cli_auth: Raw CLI JSON string for authentication.
468
-
469
- Returns:
470
- A tuple of (merged_payload, missing_required_fields).
471
-
472
- Raises:
473
- click.ClickException: If config/auth JSON parsing or validation fails.
474
- """
475
- merged_base = import_data.copy() if import_data else {}
476
-
477
- # Merge simple string fields using truthy CLI overrides
478
- for field, cli_value in (
479
- ("name", _coerce_cli_string(cli_name)),
480
- ("transport", _coerce_cli_string(cli_transport)),
481
- ("description", _coerce_cli_string(cli_description)),
482
- ):
483
- if cli_value is not None:
484
- merged_base[field] = cli_value
485
-
486
- # Determine final transport before validating config
487
- final_transport = merged_base.get("transport")
488
-
489
- # Merge config and authentication with validation
490
- _merge_config_field(merged_base, cli_config, final_transport)
491
- _merge_auth_field(merged_base, cli_auth)
492
-
493
- # Validate required fields
494
- missing_fields = []
495
- for required in ("name", "transport"):
496
- value = merged_base.get(required)
497
- if not isinstance(value, str) or not value.strip():
498
- missing_fields.append(required)
499
-
500
- return merged_base, missing_fields
501
-
502
-
503
- @mcps_group.command(name="list")
504
- @output_flags()
505
- @click.pass_context
506
- def list_mcps(ctx: Any) -> None:
507
- """List all MCPs in a formatted table.
508
-
509
- Args:
510
- ctx: Click context containing output format preferences
511
-
512
- Raises:
513
- ClickException: If API request fails
514
- """
515
- try:
516
- client = get_client(ctx)
517
- with spinner_context(
518
- ctx,
519
- "[bold blue]Fetching MCPs…[/bold blue]",
520
- console_override=console,
521
- ):
522
- mcps = client.mcps.list_mcps()
523
-
524
- # Define table columns: (data_key, header, style, width)
525
- columns = [
526
- ("id", "ID", "dim", 36),
527
- ("name", "Name", ACCENT_STYLE, None),
528
- ("config", "Config", INFO, None),
529
- ]
530
-
531
- # Transform function for safe dictionary access
532
- def transform_mcp(mcp: Any) -> dict[str, Any]:
533
- row = coerce_to_row(mcp, ["id", "name", "config"])
534
- # Ensure id is always a string
535
- row["id"] = str(row["id"])
536
- # Truncate config field for display
537
- if row["config"] != "N/A":
538
- row["config"] = str(row["config"])[:50] + "..." if len(str(row["config"])) > 50 else str(row["config"])
539
- return row
540
-
541
- output_list(ctx, mcps, "🔌 Available MCPs", columns, transform_mcp)
542
-
543
- except Exception as e:
544
- raise click.ClickException(str(e)) from e
545
-
546
-
547
- @mcps_group.command()
548
- @click.option("--name", help="MCP name")
549
- @click.option("--transport", help="MCP transport protocol")
550
- @click.option("--description", help="MCP description")
551
- @click.option(
552
- "--config",
553
- help="JSON configuration string or @file reference (e.g., @config.json)",
554
- )
555
- @click.option(
556
- "--auth",
557
- "--authentication",
558
- "auth",
559
- help="JSON authentication object or @file reference (e.g., @auth.json)",
560
- )
561
- @click.option(
562
- "--import",
563
- "import_file",
564
- type=click.Path(exists=True, dir_okay=False),
565
- help="Import MCP configuration from JSON or YAML export",
566
- )
567
- @output_flags()
568
- @click.pass_context
569
- def create(
570
- ctx: Any,
571
- name: str | None,
572
- transport: str | None,
573
- description: str | None,
574
- config: str | None,
575
- auth: str | None,
576
- import_file: str | None,
577
- ) -> None:
578
- """Create a new MCP with specified configuration.
579
-
580
- You can create an MCP by providing all parameters via CLI options, or by
581
- importing from a file and optionally overriding specific fields.
582
-
583
- Args:
584
- ctx: Click context containing output format preferences
585
- name: MCP name (required unless provided via --import)
586
- transport: MCP transport protocol (required unless provided via --import)
587
- description: Optional MCP description
588
- config: JSON configuration string or @file reference
589
- auth: JSON authentication object or @file reference
590
- import_file: Optional path to import configuration from export file.
591
- CLI options override imported values.
592
-
593
- Raises:
594
- ClickException: If JSON parsing fails or API request fails
595
-
596
- Examples:
597
- Create from CLI options:
598
- aip mcps create --name my-mcp --transport http --config '{"url": "https://api.example.com"}'
599
-
600
- Import from file:
601
- aip mcps create --import mcp-export.json
602
-
603
- Import with overrides:
604
- aip mcps create --import mcp-export.json --name new-name --transport sse
605
- """
606
- try:
607
- client = get_client(ctx)
608
-
609
- import_payload = _load_import_ready_payload(import_file) if import_file is not None else None
610
-
611
- merged_payload, missing_fields = _merge_import_payload(
612
- import_payload,
613
- cli_name=name,
614
- cli_transport=transport,
615
- cli_description=description,
616
- cli_config=config,
617
- cli_auth=auth,
618
- )
619
-
620
- if missing_fields:
621
- raise click.ClickException(
622
- "Missing required fields after combining import and CLI values: " + ", ".join(missing_fields)
623
- )
624
-
625
- effective_name = merged_payload["name"]
626
- effective_transport = merged_payload["transport"]
627
- effective_description = merged_payload.get("description")
628
- effective_config = merged_payload.get("config") or {}
629
- effective_auth = merged_payload.get("authentication")
630
-
631
- with spinner_context(
632
- ctx,
633
- "[bold blue]Creating MCP…[/bold blue]",
634
- console_override=console,
635
- ):
636
- create_kwargs: dict[str, Any] = {
637
- "name": effective_name,
638
- "config": effective_config,
639
- "transport": effective_transport,
640
- }
641
-
642
- if effective_description is not None:
643
- create_kwargs["description"] = effective_description
644
-
645
- if effective_auth:
646
- create_kwargs["authentication"] = effective_auth
647
-
648
- mcp_metadata = merged_payload.get("mcp_metadata")
649
- if mcp_metadata is not None:
650
- create_kwargs["mcp_metadata"] = mcp_metadata
651
-
652
- mcp = client.mcps.create_mcp(**create_kwargs)
653
-
654
- # Handle JSON output
655
- handle_json_output(ctx, mcp.model_dump())
656
-
657
- # Handle Rich output
658
- rich_panel = display_creation_success(
659
- "MCP",
660
- mcp.name,
661
- mcp.id,
662
- Type=getattr(mcp, "type", DEFAULT_MCP_TYPE),
663
- Transport=getattr(mcp, "transport", effective_transport),
664
- Description=effective_description or "No description",
665
- )
666
- handle_rich_output(ctx, rich_panel)
667
-
668
- except Exception as e:
669
- _handle_cli_error(ctx, e, "MCP creation")
670
-
671
-
672
- def _handle_mcp_export(
673
- ctx: Any,
674
- client: Any,
675
- mcp: Any,
676
- export_path: Path,
677
- no_auth_prompt: bool,
678
- auth_placeholder: str,
679
- ) -> None:
680
- """Handle MCP export to file with format detection and auth handling.
681
-
682
- Args:
683
- ctx: Click context for spinner management
684
- client: API client for fetching MCP details
685
- mcp: MCP object to export
686
- export_path: Target file path (format detected from extension)
687
- no_auth_prompt: Skip interactive secret prompts if True
688
- auth_placeholder: Placeholder text for missing secrets
689
-
690
- Note:
691
- Supports JSON (.json) and YAML (.yaml/.yml) export formats.
692
- In interactive mode, prompts for secret values.
693
- In non-interactive mode, uses placeholder values.
694
- """
695
- # Auto-detect format from file extension
696
- detected_format = detect_export_format(export_path)
697
-
698
- # Always export comprehensive data - re-fetch with full details
699
- try:
700
- with spinner_context(
701
- ctx,
702
- "[bold blue]Fetching complete MCP details…[/bold blue]",
703
- console_override=console,
704
- ):
705
- mcp = client.mcps.get_mcp_by_id(mcp.id)
706
- except Exception as e:
707
- print_markup(
708
- f"[{WARNING_STYLE}]⚠️ Could not fetch full MCP details: {e}[/]",
709
- console=console,
710
- )
711
- print_markup(f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]", console=console)
712
-
713
- # Determine if we should prompt for secrets
714
- prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
715
-
716
- # Warn user if non-interactive mode forces placeholder usage
717
- if not no_auth_prompt and not sys.stdin.isatty():
718
- print_markup(
719
- f"[{WARNING_STYLE}]⚠️ Non-interactive mode detected. Using placeholder values for secrets.[/]",
720
- console=console,
721
- )
722
-
723
- # Build and write export payload
724
- if prompt_for_secrets:
725
- # Interactive mode: no spinner during prompts
726
- export_payload = build_mcp_export_payload(
727
- mcp,
728
- prompt_for_secrets=prompt_for_secrets,
729
- placeholder=auth_placeholder,
730
- console=console,
731
- )
732
- with spinner_context(
733
- ctx,
734
- "[bold blue]Writing export file…[/bold blue]",
735
- console_override=console,
736
- ):
737
- write_resource_export(export_path, export_payload, detected_format)
738
- else:
739
- # Non-interactive mode: spinner for entire export process
740
- with spinner_context(
741
- ctx,
742
- "[bold blue]Exporting MCP configuration…[/bold blue]",
743
- console_override=console,
744
- ):
745
- export_payload = build_mcp_export_payload(
746
- mcp,
747
- prompt_for_secrets=prompt_for_secrets,
748
- placeholder=auth_placeholder,
749
- console=console,
750
- )
751
- write_resource_export(export_path, export_payload, detected_format)
752
-
753
- print_markup(
754
- f"[{SUCCESS_STYLE}]✅ Complete MCP configuration exported to: {export_path} (format: {detected_format})[/]",
755
- console=console,
756
- )
757
-
758
-
759
- def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
760
- """Display MCP details using raw API data or fallback to Pydantic model.
761
-
762
- Args:
763
- ctx: Click context containing output format preferences
764
- client: API client for fetching raw MCP data
765
- mcp: MCP object to display details for
766
-
767
- Note:
768
- Attempts to fetch raw API data first to preserve all fields.
769
- Falls back to Pydantic model data if raw data unavailable.
770
- Formats datetime fields for better readability.
771
- """
772
- # Try to fetch raw API data first to preserve ALL fields
773
- with spinner_context(
774
- ctx,
775
- "[bold blue]Fetching detailed MCP data…[/bold blue]",
776
- console_override=console,
777
- ):
778
- raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
779
-
780
- if raw_mcp_data:
781
- # Use raw API data - this preserves ALL fields
782
- formatted_data = raw_mcp_data.copy()
783
- if "created_at" in formatted_data:
784
- formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
785
- if "updated_at" in formatted_data:
786
- formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
787
-
788
- output_result(
789
- ctx,
790
- formatted_data,
791
- title="MCP Details",
792
- panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
793
- )
794
- else:
795
- # Fall back to Pydantic model data
796
- console.print(f"[{WARNING_STYLE}]Falling back to Pydantic model data[/]")
797
- result_data = {
798
- "id": str(getattr(mcp, "id", "N/A")),
799
- "name": getattr(mcp, "name", "N/A"),
800
- "type": getattr(mcp, "type", "N/A"),
801
- "config": getattr(mcp, "config", "N/A"),
802
- "status": getattr(mcp, "status", "N/A"),
803
- "connection_status": getattr(mcp, "connection_status", "N/A"),
804
- }
805
- output_result(ctx, result_data, title=f"🔌 {mcp.name}")
806
-
807
-
808
- @mcps_group.command()
809
- @click.argument("mcp_ref")
810
- @click.option(
811
- "--export",
812
- type=click.Path(dir_okay=False, writable=True),
813
- help="Export complete MCP configuration to file (format auto-detected from .json/.yaml extension)",
814
- )
815
- @click.option(
816
- "--no-auth-prompt",
817
- is_flag=True,
818
- help="Skip interactive secret prompts and use placeholder values.",
819
- )
820
- @click.option(
821
- "--auth-placeholder",
822
- default="<INSERT VALUE>",
823
- show_default=True,
824
- help="Placeholder text used when secrets are unavailable.",
825
- )
826
- @output_flags()
827
- @click.pass_context
828
- def get(
829
- ctx: Any,
830
- mcp_ref: str,
831
- export: str | None,
832
- no_auth_prompt: bool,
833
- auth_placeholder: str,
834
- ) -> None:
835
- """Get MCP details and optionally export configuration to file.
836
-
837
- Args:
838
- ctx: Click context containing output format preferences
839
- mcp_ref: MCP reference (ID or name)
840
- export: Optional file path to export MCP configuration
841
- no_auth_prompt: Skip interactive secret prompts if True
842
- auth_placeholder: Placeholder text for missing secrets
843
-
844
- Raises:
845
- ClickException: If MCP not found or export fails
846
-
847
- Examples:
848
- aip mcps get my-mcp
849
- aip mcps get my-mcp --export mcp.json # Export as JSON
850
- aip mcps get my-mcp --export mcp.yaml # Export as YAML
851
- """
852
- try:
853
- client = get_client(ctx)
854
-
855
- # Resolve MCP using helper function
856
- mcp = _resolve_mcp(ctx, client, mcp_ref)
857
-
858
- # Handle export option
859
- if export:
860
- _handle_mcp_export(ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder)
861
-
862
- # Display MCP details
863
- _display_mcp_details(ctx, client, mcp)
864
-
865
- except Exception as e:
866
- raise click.ClickException(str(e)) from e
867
-
868
-
869
- @mcps_group.command("tools")
870
- @click.argument("mcp_ref")
871
- @output_flags()
872
- @click.pass_context
873
- def list_tools(ctx: Any, mcp_ref: str) -> None:
874
- """List tools available from a specific MCP.
875
-
876
- Args:
877
- ctx: Click context containing output format preferences
878
- mcp_ref: MCP reference (ID or name)
879
-
880
- Raises:
881
- ClickException: If MCP not found or tools fetch fails
882
- """
883
- try:
884
- client = get_client(ctx)
885
-
886
- # Resolve MCP using helper function
887
- mcp = _resolve_mcp(ctx, client, mcp_ref)
888
-
889
- # Get tools from MCP
890
- with spinner_context(
891
- ctx,
892
- "[bold blue]Fetching MCP tools…[/bold blue]",
893
- console_override=console,
894
- ):
895
- tools = client.mcps.get_mcp_tools(mcp.id)
896
-
897
- # Define table columns: (data_key, header, style, width)
898
- columns = [
899
- ("name", "Name", ACCENT_STYLE, None),
900
- ("description", "Description", INFO, 50),
901
- ]
902
-
903
- # Transform function for safe dictionary access
904
- def transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
905
- return {
906
- "name": tool.get("name", "N/A"),
907
- "description": tool.get("description", "N/A")[:47] + "..."
908
- if len(tool.get("description", "")) > 47
909
- else tool.get("description", "N/A"),
910
- }
911
-
912
- output_list(
913
- ctx,
914
- tools,
915
- f"{ICON_TOOL} Tools from MCP: {mcp.name}",
916
- columns,
917
- transform_tool,
918
- )
919
-
920
- except Exception as e:
921
- raise click.ClickException(str(e)) from e
922
-
923
-
924
- @mcps_group.command("connect")
925
- @click.option(
926
- "--from-file",
927
- "config_file",
928
- required=True,
929
- help="MCP config JSON file",
930
- )
931
- @output_flags()
932
- @click.pass_context
933
- def connect(ctx: Any, config_file: str) -> None:
934
- """Test MCP connection using a configuration file.
935
-
936
- Args:
937
- ctx: Click context containing output format preferences
938
- config_file: Path to MCP configuration JSON file
939
-
940
- Raises:
941
- ClickException: If config file invalid or connection test fails
942
-
943
- Note:
944
- Loads MCP configuration from JSON file and tests connectivity.
945
- Displays success or failure with connection details.
946
- """
947
- try:
948
- client = get_client(ctx)
949
-
950
- # Load MCP config from file
951
- with open(config_file) as f:
952
- config = json.load(f)
953
-
954
- view = get_ctx_value(ctx, "view", "rich")
955
- if view != "json":
956
- print_markup(
957
- f"[{WARNING_STYLE}]Connecting to MCP with config from {config_file}...[/]",
958
- console=console,
959
- )
960
-
961
- # Test connection using config
962
- with spinner_context(
963
- ctx,
964
- "[bold blue]Connecting to MCP…[/bold blue]",
965
- console_override=console,
966
- ):
967
- result = client.mcps.test_mcp_connection_from_config(config)
968
-
969
- view = get_ctx_value(ctx, "view", "rich")
970
- if view == "json":
971
- handle_json_output(ctx, result)
972
- else:
973
- success_panel = AIPPanel(
974
- f"[{SUCCESS_STYLE}]✓[/] MCP connection successful!\n\n[bold]Result:[/bold] {result}",
975
- title="🔌 Connection",
976
- border_style=SUCCESS,
977
- )
978
- console.print(success_panel)
979
-
980
- except Exception as e:
981
- raise click.ClickException(str(e)) from e
982
-
983
-
984
- def _generate_update_preview(mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]) -> str:
985
- """Generate formatted preview of changes for user confirmation.
986
-
987
- Args:
988
- mcp: Current MCP object
989
- update_data: Data that will be sent in update request
990
- cli_overrides: CLI flags that were explicitly provided
991
-
992
- Returns:
993
- Formatted preview string showing old→new values
994
- """
995
- lines = [f"\n[bold]The following fields will be updated for MCP '{mcp.name}':[/bold]\n"]
996
-
997
- empty_overrides = []
998
-
999
- # Show each field that will be updated
1000
- for field, new_value in update_data.items():
1001
- old_value = getattr(mcp, field, None)
1002
-
1003
- # Track empty CLI overrides
1004
- if field in cli_overrides and cli_overrides[field] == "":
1005
- empty_overrides.append(field)
1006
-
1007
- old_display = _format_preview_value(old_value)
1008
- new_display = _format_preview_value(new_value)
1009
-
1010
- lines.append(f"- [cyan]{field}[/cyan]: {old_display} → {new_display}")
1011
-
1012
- # Add warnings for empty CLI overrides
1013
- lines.extend(_build_empty_override_warnings(empty_overrides))
1014
-
1015
- return "\n".join(lines)
1016
-
1017
-
1018
- @mcps_group.command()
1019
- @click.argument("mcp_ref")
1020
- @click.option("--name", help="New MCP name")
1021
- @click.option("--transport", type=click.Choice(["http", "sse"]), help="New transport protocol")
1022
- @click.option("--description", help="New description")
1023
- @click.option(
1024
- "--config",
1025
- help="JSON configuration string or @file reference (e.g., @config.json)",
1026
- )
1027
- @click.option(
1028
- "--auth",
1029
- "--authentication",
1030
- "auth",
1031
- help="JSON authentication object or @file reference (e.g., @auth.json)",
1032
- )
1033
- @click.option(
1034
- "--import",
1035
- "import_file",
1036
- type=click.Path(exists=True, dir_okay=False, readable=True),
1037
- help="Import MCP configuration from JSON or YAML export",
1038
- )
1039
- @click.option("-y", is_flag=True, help="Skip confirmation prompt when using --import")
1040
- @output_flags()
1041
- @click.pass_context
1042
- def update(
1043
- ctx: Any,
1044
- mcp_ref: str,
1045
- name: str | None,
1046
- transport: str | None,
1047
- description: str | None,
1048
- config: str | None,
1049
- auth: str | None,
1050
- import_file: str | None,
1051
- y: bool,
1052
- ) -> None:
1053
- """Update an existing MCP with new configuration values.
1054
-
1055
- You can update an MCP by providing individual fields via CLI options, or by
1056
- importing from a file and optionally overriding specific fields.
1057
-
1058
- Args:
1059
- ctx: Click context containing output format preferences
1060
- mcp_ref: MCP reference (ID or name)
1061
- name: New MCP name (optional)
1062
- transport: New transport protocol (optional)
1063
- description: New description (optional)
1064
- config: New JSON configuration string or @file reference (optional)
1065
- auth: New JSON authentication object or @file reference (optional)
1066
- import_file: Optional path to import configuration from export file.
1067
- CLI options override imported values.
1068
- y: Skip confirmation prompt when using --import
1069
-
1070
- Raises:
1071
- ClickException: If MCP not found, JSON invalid, or no fields specified
1072
-
1073
- Note:
1074
- Must specify either --import OR at least one CLI field.
1075
- CLI options override imported values when both are specified.
1076
- Uses PATCH for import-based updates, PUT/PATCH for CLI-only updates.
1077
-
1078
- Examples:
1079
- Update with CLI options:
1080
- aip mcps update my-mcp --name new-name --transport sse
1081
-
1082
- Import from file:
1083
- aip mcps update my-mcp --import mcp-export.json
1084
-
1085
- Import with overrides:
1086
- aip mcps update my-mcp --import mcp-export.json --name new-name -y
1087
- """
1088
- try:
1089
- client = get_client(ctx)
1090
-
1091
- # Validate that at least one update method is provided
1092
- cli_flags_provided = any(v is not None for v in [name, transport, description, config, auth])
1093
- if not import_file and not cli_flags_provided:
1094
- raise click.ClickException(
1095
- "No update fields specified. Use --import or one of: "
1096
- "--name, --transport, --description, --config, --auth"
1097
- )
1098
-
1099
- # Resolve MCP using helper function
1100
- mcp = _resolve_mcp(ctx, client, mcp_ref)
1101
-
1102
- # Load and validate import data if provided
1103
- import_payload = None
1104
- if import_file:
1105
- import_payload = _load_import_ready_payload(import_file)
1106
- if not _validate_import_payload_fields(import_payload):
1107
- return
1108
-
1109
- # Build update data from import and CLI flags
1110
- update_data = _build_update_data_from_sources(import_payload, mcp, name, transport, description, config, auth)
1111
-
1112
- if not update_data:
1113
- raise click.ClickException("No update fields specified")
1114
-
1115
- # Show confirmation preview for import-based updates (unless -y flag)
1116
- if import_payload and not y:
1117
- cli_overrides = _collect_cli_overrides(name, transport, description, config, auth)
1118
- preview = _generate_update_preview(mcp, update_data, cli_overrides)
1119
- print_markup(preview)
1120
-
1121
- if not click.confirm("\nContinue with update?", default=False):
1122
- print_markup("[yellow]Update cancelled.[/yellow]")
1123
- return
1124
-
1125
- # Update MCP
1126
- with spinner_context(
1127
- ctx,
1128
- "[bold blue]Updating MCP…[/bold blue]",
1129
- console_override=console,
1130
- ):
1131
- updated_mcp = client.mcps.update_mcp(mcp.id, **update_data)
1132
-
1133
- handle_json_output(ctx, updated_mcp.model_dump())
1134
- handle_rich_output(ctx, display_update_success("MCP", updated_mcp.name))
1135
-
1136
- except Exception as e:
1137
- _handle_cli_error(ctx, e, "MCP update")
1138
-
1139
-
1140
- @mcps_group.command()
1141
- @click.argument("mcp_ref")
1142
- @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
1143
- @output_flags()
1144
- @click.pass_context
1145
- def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
1146
- """Delete an MCP after confirmation.
1147
-
1148
- Args:
1149
- ctx: Click context containing output format preferences
1150
- mcp_ref: MCP reference (ID or name)
1151
- yes: Skip confirmation prompt if True
1152
-
1153
- Raises:
1154
- ClickException: If MCP not found or deletion fails
1155
-
1156
- Note:
1157
- Requires confirmation unless --yes flag is provided.
1158
- Deletion is permanent and cannot be undone.
1159
- """
1160
- try:
1161
- client = get_client(ctx)
1162
-
1163
- # Resolve MCP using helper function
1164
- mcp = _resolve_mcp(ctx, client, mcp_ref)
1165
-
1166
- # Confirm deletion
1167
- if not yes and not display_confirmation_prompt("MCP", mcp.name):
1168
- return
1169
-
1170
- with spinner_context(
1171
- ctx,
1172
- "[bold blue]Deleting MCP…[/bold blue]",
1173
- console_override=console,
1174
- ):
1175
- client.mcps.delete_mcp(mcp.id)
1176
-
1177
- handle_json_output(
1178
- ctx,
1179
- {
1180
- "success": True,
1181
- "message": f"MCP '{mcp.name}' deleted",
1182
- },
1183
- )
1184
- handle_rich_output(ctx, display_deletion_success("MCP", mcp.name))
1185
-
1186
- except Exception as e:
1187
- _handle_cli_error(ctx, e, "MCP deletion")