general-augment-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- general_augment_cli-0.1.0.dist-info/METADATA +180 -0
- general_augment_cli-0.1.0.dist-info/RECORD +42 -0
- general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
- general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
- platform_cli/__init__.py +5 -0
- platform_cli/branding.py +27 -0
- platform_cli/client.py +179 -0
- platform_cli/commands/__init__.py +1 -0
- platform_cli/commands/approvals.py +150 -0
- platform_cli/commands/auth.py +96 -0
- platform_cli/commands/billing.py +143 -0
- platform_cli/commands/channels.py +212 -0
- platform_cli/commands/deploy.py +72 -0
- platform_cli/commands/dev.py +38 -0
- platform_cli/commands/doctor.py +170 -0
- platform_cli/commands/identity.py +433 -0
- platform_cli/commands/init.py +55 -0
- platform_cli/commands/integrate.py +94 -0
- platform_cli/commands/keys.py +116 -0
- platform_cli/commands/logs.py +43 -0
- platform_cli/commands/mcp.py +258 -0
- platform_cli/commands/memory.py +316 -0
- platform_cli/commands/mock.py +30 -0
- platform_cli/commands/model_providers.py +226 -0
- platform_cli/commands/observability.py +174 -0
- platform_cli/commands/onboarding.py +72 -0
- platform_cli/commands/projects.py +302 -0
- platform_cli/commands/skills.py +116 -0
- platform_cli/commands/smoke.py +280 -0
- platform_cli/commands/status.py +49 -0
- platform_cli/commands/tools.py +179 -0
- platform_cli/commands/users.py +150 -0
- platform_cli/commands/validate.py +96 -0
- platform_cli/commands/verify.py +648 -0
- platform_cli/config.py +114 -0
- platform_cli/errors.py +103 -0
- platform_cli/local_mock.py +1392 -0
- platform_cli/main.py +130 -0
- platform_cli/openapi.py +1048 -0
- platform_cli/output.py +47 -0
- platform_cli/readiness.py +176 -0
- platform_cli/runtime.py +22 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""OpenAPI integration scaffolding command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from platform_cli.client import encode_path_segment
|
|
11
|
+
from platform_cli.openapi import scaffold_from_openapi
|
|
12
|
+
from platform_cli.output import print_success, print_warning, table
|
|
13
|
+
from platform_cli.runtime import Runtime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def integrate(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
spec_url: Annotated[str, typer.Argument(help="OpenAPI spec URL or file path.")],
|
|
19
|
+
name: Annotated[str | None, typer.Option(help="Agent name.")] = None,
|
|
20
|
+
output_dir: Annotated[Path | None, typer.Option(help="Output directory.")] = None,
|
|
21
|
+
description: Annotated[
|
|
22
|
+
str | None,
|
|
23
|
+
typer.Option(help="Agent personality description."),
|
|
24
|
+
] = None,
|
|
25
|
+
target_count: Annotated[int, typer.Option(help="Target generated tool count.")] = 15,
|
|
26
|
+
force: Annotated[bool, typer.Option(help="Overwrite existing generated files.")] = False,
|
|
27
|
+
auto_deploy: Annotated[
|
|
28
|
+
bool,
|
|
29
|
+
typer.Option(help="Deploy immediately after scaffolding."),
|
|
30
|
+
] = False,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Generate a local agent scaffold from an OpenAPI spec."""
|
|
33
|
+
runtime: Runtime = ctx.obj
|
|
34
|
+
result = scaffold_from_openapi(
|
|
35
|
+
spec_url,
|
|
36
|
+
output_dir=output_dir,
|
|
37
|
+
name=name,
|
|
38
|
+
description=description,
|
|
39
|
+
target_count=target_count,
|
|
40
|
+
force=force,
|
|
41
|
+
)
|
|
42
|
+
rows: list[list[object]] = [
|
|
43
|
+
[
|
|
44
|
+
"disabled" if not tool.enabled else "enabled",
|
|
45
|
+
tool.tool_id,
|
|
46
|
+
tool.http_method,
|
|
47
|
+
tool.risk_level,
|
|
48
|
+
"yes" if tool.requires_approval else "no",
|
|
49
|
+
]
|
|
50
|
+
for tool in result.tools
|
|
51
|
+
]
|
|
52
|
+
table(
|
|
53
|
+
f"Generated {len(result.tools)} tools from {len(result.parsed_api.tools)} endpoints",
|
|
54
|
+
["State", "Tool", "Method", "Risk", "Approval"],
|
|
55
|
+
rows,
|
|
56
|
+
)
|
|
57
|
+
print_success(f"Generated scaffold in {result.root}")
|
|
58
|
+
print_success(f"Coding-agent handoff written to {result.agent_prompt_path}")
|
|
59
|
+
if any(not tool.enabled for tool in result.tools):
|
|
60
|
+
print_warning("Destructive operations are disabled by default.")
|
|
61
|
+
if auto_deploy:
|
|
62
|
+
from platform_cli.commands.deploy import deploy_path
|
|
63
|
+
|
|
64
|
+
project = deploy_path(runtime, result.config_path, project_ref=None)
|
|
65
|
+
project_id = project.get("id")
|
|
66
|
+
if not project_id:
|
|
67
|
+
print_warning(
|
|
68
|
+
"Project deployed, but OpenAPI tools were not registered: missing project id."
|
|
69
|
+
)
|
|
70
|
+
return
|
|
71
|
+
with runtime.client() as client:
|
|
72
|
+
response = client.admin(
|
|
73
|
+
"POST",
|
|
74
|
+
f"/projects/{encode_path_segment(str(project_id))}/tools/from-openapi",
|
|
75
|
+
json={
|
|
76
|
+
"spec_url": _deployable_spec_source(spec_url),
|
|
77
|
+
"target_count": target_count,
|
|
78
|
+
"auto_deploy": True,
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
generated = response.get("generated_count", 0)
|
|
82
|
+
curated = response.get("curated_count", 0)
|
|
83
|
+
enabled = len(response.get("enabled_tool_ids") or [])
|
|
84
|
+
print_success(
|
|
85
|
+
f"Registered OpenAPI tools: {enabled} enabled, {curated} curated, {generated} generated"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _deployable_spec_source(spec_url: str) -> str:
|
|
90
|
+
"""Return a server-readable OpenAPI source for hosted registration."""
|
|
91
|
+
spec_path = Path(spec_url).expanduser()
|
|
92
|
+
if spec_path.exists() and spec_path.is_file():
|
|
93
|
+
return spec_path.read_text(encoding="utf-8")
|
|
94
|
+
return spec_url
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""API key management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from platform_cli.client import encode_path_segment, resolve_project
|
|
10
|
+
from platform_cli.output import panel, print_success, print_warning, table
|
|
11
|
+
from platform_cli.runtime import Runtime
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Manage project-scoped API keys.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_keys(ctx: typer.Context) -> None:
|
|
18
|
+
"""List masked API keys visible to this management credential."""
|
|
19
|
+
|
|
20
|
+
runtime: Runtime = ctx.obj
|
|
21
|
+
with runtime.client() as client:
|
|
22
|
+
payload = client.admin("GET", "/keys")
|
|
23
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
24
|
+
rows = [
|
|
25
|
+
[
|
|
26
|
+
item.get("name", ""),
|
|
27
|
+
item.get("masked_key", ""),
|
|
28
|
+
item.get("project_id", ""),
|
|
29
|
+
",".join(item.get("scopes", [])),
|
|
30
|
+
item.get("expires_at", "") or "",
|
|
31
|
+
item.get("id", ""),
|
|
32
|
+
]
|
|
33
|
+
for item in items
|
|
34
|
+
if isinstance(item, dict)
|
|
35
|
+
]
|
|
36
|
+
table("API Keys", ["Name", "Key", "Project", "Scopes", "Expires", "ID"], rows)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("create")
|
|
40
|
+
def create_key(
|
|
41
|
+
ctx: typer.Context,
|
|
42
|
+
name: str = typer.Option(..., help="Display name, for example Production backend."),
|
|
43
|
+
project: str | None = typer.Option(None, help="Project id, slug, or name."),
|
|
44
|
+
scope: Annotated[
|
|
45
|
+
list[str] | None,
|
|
46
|
+
typer.Option("--scope", help="Scope to grant; repeatable."),
|
|
47
|
+
] = None,
|
|
48
|
+
expires_at: str | None = typer.Option(None, help="Optional ISO-8601 expiration timestamp."),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Create an API key and print the raw secret once."""
|
|
51
|
+
|
|
52
|
+
runtime: Runtime = ctx.obj
|
|
53
|
+
payload: dict[str, object] = {
|
|
54
|
+
"name": name,
|
|
55
|
+
"scopes": scope or ["admin"],
|
|
56
|
+
}
|
|
57
|
+
with runtime.client() as client:
|
|
58
|
+
if project:
|
|
59
|
+
project_payload = resolve_project(client, project)
|
|
60
|
+
payload["project_id"] = str(project_payload["id"])
|
|
61
|
+
if expires_at is not None:
|
|
62
|
+
payload["expires_at"] = expires_at
|
|
63
|
+
response = client.admin("POST", "/keys", json=payload)
|
|
64
|
+
print_success(
|
|
65
|
+
f"Created API key {response.get('name', name)} ({response.get('id', 'unknown')})."
|
|
66
|
+
)
|
|
67
|
+
print_warning("The raw API key is shown once. Store it in your backend secret manager.")
|
|
68
|
+
panel(
|
|
69
|
+
"API Key",
|
|
70
|
+
f"api_key: {response.get('api_key', '')}\n"
|
|
71
|
+
f"masked_key: {response.get('masked_key', '')}\n"
|
|
72
|
+
f"project_id: {response.get('project_id', payload.get('project_id', ''))}\n"
|
|
73
|
+
f"scopes: {','.join(response.get('scopes', []))}",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("update")
|
|
78
|
+
def update_key(
|
|
79
|
+
ctx: typer.Context,
|
|
80
|
+
key_id: str = typer.Argument(..., help="API key id."),
|
|
81
|
+
name: str | None = typer.Option(None, help="New display name."),
|
|
82
|
+
scope: Annotated[
|
|
83
|
+
list[str] | None,
|
|
84
|
+
typer.Option("--scope", help="Replacement scope; repeatable."),
|
|
85
|
+
] = None,
|
|
86
|
+
expires_at: str | None = typer.Option(None, help="Replacement ISO-8601 expiration timestamp."),
|
|
87
|
+
clear_expiration: bool = typer.Option(False, help="Clear the key expiration."),
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Update API key metadata without showing the raw secret."""
|
|
90
|
+
|
|
91
|
+
runtime: Runtime = ctx.obj
|
|
92
|
+
payload: dict[str, object | None] = {}
|
|
93
|
+
if name is not None:
|
|
94
|
+
payload["name"] = name
|
|
95
|
+
if scope is not None:
|
|
96
|
+
payload["scopes"] = scope
|
|
97
|
+
if clear_expiration:
|
|
98
|
+
payload["expires_at"] = None
|
|
99
|
+
elif expires_at is not None:
|
|
100
|
+
payload["expires_at"] = expires_at
|
|
101
|
+
with runtime.client() as client:
|
|
102
|
+
response = client.admin("PATCH", f"/keys/{encode_path_segment(key_id)}", json=payload)
|
|
103
|
+
print_success(f"Updated API key {response.get('name', key_id)}.")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command("revoke")
|
|
107
|
+
def revoke_key(
|
|
108
|
+
ctx: typer.Context,
|
|
109
|
+
key_id: str = typer.Argument(..., help="API key id."),
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Revoke an API key."""
|
|
112
|
+
|
|
113
|
+
runtime: Runtime = ctx.obj
|
|
114
|
+
with runtime.client() as client:
|
|
115
|
+
response = client.admin("DELETE", f"/keys/{encode_path_segment(key_id)}")
|
|
116
|
+
print_success(f"Revoked API key {response.get('id', key_id)}.")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Project log streaming command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from platform_cli.client import encode_path_segment, resolve_project
|
|
10
|
+
from platform_cli.output import table
|
|
11
|
+
from platform_cli.runtime import Runtime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def logs(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
project: str = typer.Option(..., help="Project id, slug, or name."),
|
|
17
|
+
follow: bool = typer.Option(False, "--follow", help="Poll continuously."),
|
|
18
|
+
limit: int = typer.Option(25, min=1, max=200, help="Log row limit."),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Show recent project logs."""
|
|
21
|
+
runtime: Runtime = ctx.obj
|
|
22
|
+
with runtime.client() as client:
|
|
23
|
+
project_payload = resolve_project(client, project)
|
|
24
|
+
while True:
|
|
25
|
+
payload = client.admin(
|
|
26
|
+
"GET",
|
|
27
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/logs",
|
|
28
|
+
params={"limit": limit},
|
|
29
|
+
)
|
|
30
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
31
|
+
rows = [
|
|
32
|
+
[
|
|
33
|
+
item.get("created_at", ""),
|
|
34
|
+
item.get("role", ""),
|
|
35
|
+
str(item.get("content", ""))[:80],
|
|
36
|
+
]
|
|
37
|
+
for item in items
|
|
38
|
+
if isinstance(item, dict)
|
|
39
|
+
]
|
|
40
|
+
table("Logs", ["Time", "Role", "Content"], rows)
|
|
41
|
+
if not follow:
|
|
42
|
+
return
|
|
43
|
+
time.sleep(2)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""MCP server management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from platform_cli.client import encode_path_segment, resolve_project
|
|
10
|
+
from platform_cli.output import print_json, print_success, table
|
|
11
|
+
from platform_cli.runtime import Runtime
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Manage tenant MCP servers.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_mcp_servers(
|
|
18
|
+
ctx: typer.Context,
|
|
19
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
20
|
+
json_output: Annotated[
|
|
21
|
+
bool,
|
|
22
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
23
|
+
] = False,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""List MCP servers configured for one project."""
|
|
26
|
+
runtime: Runtime = ctx.obj
|
|
27
|
+
with runtime.client() as client:
|
|
28
|
+
project_payload = resolve_project(client, project)
|
|
29
|
+
payload = client.admin(
|
|
30
|
+
"GET",
|
|
31
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers",
|
|
32
|
+
)
|
|
33
|
+
if json_output:
|
|
34
|
+
print_json(payload)
|
|
35
|
+
return
|
|
36
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
37
|
+
rows = [_server_row(item) for item in items if isinstance(item, dict)]
|
|
38
|
+
table("MCP servers", ["Name", "Transport", "Enabled", "Endpoint", "Tools"], rows)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("add")
|
|
42
|
+
def add_mcp_server(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
name: Annotated[str, typer.Argument(help="MCP server name.")],
|
|
45
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
46
|
+
url: Annotated[str | None, typer.Option(help="HTTP MCP endpoint URL.")] = None,
|
|
47
|
+
command: Annotated[str | None, typer.Option(help="Stdio MCP command.")] = None,
|
|
48
|
+
arg: Annotated[
|
|
49
|
+
list[str] | None,
|
|
50
|
+
typer.Option("--arg", help="Stdio command argument. Repeatable."),
|
|
51
|
+
] = None,
|
|
52
|
+
header: Annotated[
|
|
53
|
+
list[str] | None,
|
|
54
|
+
typer.Option("--header", help="HTTP header as key=value. Repeatable."),
|
|
55
|
+
] = None,
|
|
56
|
+
env: Annotated[
|
|
57
|
+
list[str] | None,
|
|
58
|
+
typer.Option("--env", help="Stdio environment value as key=value. Repeatable."),
|
|
59
|
+
] = None,
|
|
60
|
+
include_tool: Annotated[
|
|
61
|
+
list[str] | None,
|
|
62
|
+
typer.Option("--include-tool", help="MCP tool name to expose. Repeatable."),
|
|
63
|
+
] = None,
|
|
64
|
+
exclude_tool: Annotated[
|
|
65
|
+
list[str] | None,
|
|
66
|
+
typer.Option("--exclude-tool", help="MCP tool name to hide. Repeatable."),
|
|
67
|
+
] = None,
|
|
68
|
+
timeout: Annotated[int | None, typer.Option(min=1, help="Runtime timeout seconds.")] = None,
|
|
69
|
+
connect_timeout: Annotated[
|
|
70
|
+
int | None,
|
|
71
|
+
typer.Option("--connect-timeout", min=1, help="Connection timeout seconds."),
|
|
72
|
+
] = None,
|
|
73
|
+
enabled: Annotated[
|
|
74
|
+
bool,
|
|
75
|
+
typer.Option("--enabled/--disabled", help="Whether Hermes may use this server."),
|
|
76
|
+
] = True,
|
|
77
|
+
json_output: Annotated[
|
|
78
|
+
bool,
|
|
79
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
80
|
+
] = False,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Add one HTTP or stdio MCP server to a project."""
|
|
83
|
+
if bool(url) == bool(command):
|
|
84
|
+
raise typer.BadParameter("Provide exactly one transport: --url or --command.")
|
|
85
|
+
payload: dict[str, Any] = {"name": name, "enabled": enabled}
|
|
86
|
+
if url:
|
|
87
|
+
payload["url"] = url
|
|
88
|
+
if command:
|
|
89
|
+
payload["command"] = command
|
|
90
|
+
if arg:
|
|
91
|
+
payload["args"] = list(arg)
|
|
92
|
+
headers = _key_value_pairs(header or [], option="--header")
|
|
93
|
+
if headers:
|
|
94
|
+
payload["headers"] = headers
|
|
95
|
+
env_values = _key_value_pairs(env or [], option="--env")
|
|
96
|
+
if env_values:
|
|
97
|
+
payload["env"] = env_values
|
|
98
|
+
tools = _tools_filter(include_tool or [], exclude_tool or [])
|
|
99
|
+
if tools:
|
|
100
|
+
payload["tools"] = tools
|
|
101
|
+
if timeout is not None:
|
|
102
|
+
payload["timeout"] = timeout
|
|
103
|
+
if connect_timeout is not None:
|
|
104
|
+
payload["connect_timeout"] = connect_timeout
|
|
105
|
+
runtime: Runtime = ctx.obj
|
|
106
|
+
with runtime.client() as client:
|
|
107
|
+
project_payload = resolve_project(client, project)
|
|
108
|
+
response = client.admin(
|
|
109
|
+
"POST",
|
|
110
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers",
|
|
111
|
+
json=payload,
|
|
112
|
+
)
|
|
113
|
+
if json_output:
|
|
114
|
+
print_json(response)
|
|
115
|
+
return
|
|
116
|
+
table(
|
|
117
|
+
"Added MCP server",
|
|
118
|
+
["Field", "Value"],
|
|
119
|
+
[
|
|
120
|
+
["Project", project],
|
|
121
|
+
["Name", _value(response, "name") or name],
|
|
122
|
+
["Transport", _transport(response if isinstance(response, dict) else payload)],
|
|
123
|
+
["Enabled", _value(response, "enabled")],
|
|
124
|
+
["Tools", _tools_label(response if isinstance(response, dict) else payload)],
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command("test")
|
|
130
|
+
def test_mcp_server(
|
|
131
|
+
ctx: typer.Context,
|
|
132
|
+
name: Annotated[str, typer.Argument(help="MCP server name.")],
|
|
133
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
134
|
+
json_output: Annotated[
|
|
135
|
+
bool,
|
|
136
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
137
|
+
] = False,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Test one configured MCP server."""
|
|
140
|
+
runtime: Runtime = ctx.obj
|
|
141
|
+
with runtime.client() as client:
|
|
142
|
+
project_payload = resolve_project(client, project)
|
|
143
|
+
response = client.admin(
|
|
144
|
+
"POST",
|
|
145
|
+
(
|
|
146
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers/"
|
|
147
|
+
f"{encode_path_segment(name)}/test"
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
if json_output:
|
|
151
|
+
print_json(response)
|
|
152
|
+
return
|
|
153
|
+
table(
|
|
154
|
+
"MCP server test",
|
|
155
|
+
["Field", "Value"],
|
|
156
|
+
[
|
|
157
|
+
["Name", _value(response, "name")],
|
|
158
|
+
["OK", _value(response, "ok")],
|
|
159
|
+
["Transport", _value(response, "transport")],
|
|
160
|
+
["Detail", _value(response, "detail")],
|
|
161
|
+
],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command("delete")
|
|
166
|
+
def delete_mcp_server(
|
|
167
|
+
ctx: typer.Context,
|
|
168
|
+
name: Annotated[str, typer.Argument(help="MCP server name.")],
|
|
169
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
170
|
+
json_output: Annotated[
|
|
171
|
+
bool,
|
|
172
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
173
|
+
] = False,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Delete one configured MCP server."""
|
|
176
|
+
runtime: Runtime = ctx.obj
|
|
177
|
+
with runtime.client() as client:
|
|
178
|
+
project_payload = resolve_project(client, project)
|
|
179
|
+
response = client.admin(
|
|
180
|
+
"DELETE",
|
|
181
|
+
(
|
|
182
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers/"
|
|
183
|
+
f"{encode_path_segment(name)}"
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
if json_output:
|
|
187
|
+
print_json(response)
|
|
188
|
+
return
|
|
189
|
+
deleted_name = response.get("name", name) if isinstance(response, dict) else name
|
|
190
|
+
print_success(f"Deleted MCP server {deleted_name}.")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _key_value_pairs(values: list[str], *, option: str) -> dict[str, str]:
|
|
194
|
+
"""Parse repeated key=value CLI flags."""
|
|
195
|
+
parsed: dict[str, str] = {}
|
|
196
|
+
for item in values:
|
|
197
|
+
key, separator, value = item.partition("=")
|
|
198
|
+
if not separator or not key.strip():
|
|
199
|
+
raise typer.BadParameter(f"{option} values must use key=value.")
|
|
200
|
+
parsed[key.strip()] = value
|
|
201
|
+
return parsed
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _tools_filter(include: list[str], exclude: list[str]) -> dict[str, list[str]]:
|
|
205
|
+
"""Return MCP tool include/exclude filters."""
|
|
206
|
+
tools: dict[str, list[str]] = {}
|
|
207
|
+
if include:
|
|
208
|
+
tools["include"] = include
|
|
209
|
+
if exclude:
|
|
210
|
+
tools["exclude"] = exclude
|
|
211
|
+
return tools
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _server_row(server: dict[str, Any]) -> list[object]:
|
|
215
|
+
"""Return a table row for one MCP server."""
|
|
216
|
+
return [
|
|
217
|
+
server.get("name", ""),
|
|
218
|
+
_transport(server),
|
|
219
|
+
"yes" if server.get("enabled", True) else "no",
|
|
220
|
+
server.get("url") or server.get("command") or "",
|
|
221
|
+
_tools_label(server),
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _transport(server: dict[str, Any]) -> str:
|
|
226
|
+
"""Return MCP transport label."""
|
|
227
|
+
if server.get("url"):
|
|
228
|
+
return "http"
|
|
229
|
+
if server.get("command"):
|
|
230
|
+
return "stdio"
|
|
231
|
+
return "unknown"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _tools_label(server: dict[str, Any]) -> str:
|
|
235
|
+
"""Return a compact tool filter label."""
|
|
236
|
+
tools = server.get("tools")
|
|
237
|
+
if not isinstance(tools, dict):
|
|
238
|
+
return "all"
|
|
239
|
+
include = _string_list(tools.get("include"))
|
|
240
|
+
exclude = _string_list(tools.get("exclude"))
|
|
241
|
+
labels: list[str] = []
|
|
242
|
+
if include:
|
|
243
|
+
labels.append("include: " + ", ".join(include))
|
|
244
|
+
if exclude:
|
|
245
|
+
labels.append("exclude: " + ", ".join(exclude))
|
|
246
|
+
return "; ".join(labels) if labels else "all"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _string_list(value: object) -> list[str]:
|
|
250
|
+
"""Return a string list from JSON."""
|
|
251
|
+
return [str(item) for item in value] if isinstance(value, list) else []
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _value(payload: object, key: str) -> object:
|
|
255
|
+
"""Safely read a value from a response mapping."""
|
|
256
|
+
if isinstance(payload, dict):
|
|
257
|
+
return payload.get(key, "")
|
|
258
|
+
return ""
|