lightnow-cli 1.0.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.
- lightnow_cli/__init__.py +3 -0
- lightnow_cli/authenticated_http.py +134 -0
- lightnow_cli/client.py +250 -0
- lightnow_cli/commands/__init__.py +1 -0
- lightnow_cli/commands/auth.py +500 -0
- lightnow_cli/commands/integrations.py +658 -0
- lightnow_cli/commands/publish.py +111 -0
- lightnow_cli/commands/query.py +268 -0
- lightnow_cli/commands/runner.py +399 -0
- lightnow_cli/commands/validate.py +78 -0
- lightnow_cli/config.py +152 -0
- lightnow_cli/main.py +54 -0
- lightnow_cli/tls.py +8 -0
- lightnow_cli/validation.py +238 -0
- lightnow_cli-1.0.0.dist-info/METADATA +143 -0
- lightnow_cli-1.0.0.dist-info/RECORD +19 -0
- lightnow_cli-1.0.0.dist-info/WHEEL +4 -0
- lightnow_cli-1.0.0.dist-info/entry_points.txt +2 -0
- lightnow_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Publish command for uploading MCP server artifacts."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from ..client import get_client
|
|
12
|
+
from ..validation import validate_artifacts
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def publish(
|
|
18
|
+
server: Annotated[Path, typer.Option("--server", help="Path to server.json file")],
|
|
19
|
+
docs: Annotated[
|
|
20
|
+
Optional[Path], typer.Option("--docs", help="Path to docs.md file")
|
|
21
|
+
] = None,
|
|
22
|
+
spec: Annotated[
|
|
23
|
+
Optional[Path],
|
|
24
|
+
typer.Option("--spec", help="Path to OpenAPI specification file"),
|
|
25
|
+
] = None,
|
|
26
|
+
tenant: Annotated[
|
|
27
|
+
Optional[str], typer.Option("--tenant", help="Tenant context")
|
|
28
|
+
] = None,
|
|
29
|
+
validate_only: Annotated[
|
|
30
|
+
bool, typer.Option("--validate-only", help="Only validate files, don't publish")
|
|
31
|
+
] = False,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Publish MCP server artifacts to the registry."""
|
|
34
|
+
console.print("[bold blue]Publishing MCP server...[/bold blue]")
|
|
35
|
+
|
|
36
|
+
# Validate all artifacts first
|
|
37
|
+
console.print("Validating artifacts...")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
validation_results = validate_artifacts(
|
|
41
|
+
server_file=server, docs_file=docs, spec_file=spec
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if not validation_results["valid"]:
|
|
45
|
+
console.print(
|
|
46
|
+
f"\n[bold red]Validation failed with {len(validation_results['errors'])} error(s):[/bold red]"
|
|
47
|
+
)
|
|
48
|
+
for error in validation_results["errors"]:
|
|
49
|
+
console.print(f" [red]•[/red] {error}")
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
if validation_results["warnings"]:
|
|
53
|
+
console.print("\n[yellow]Warnings:[/yellow]")
|
|
54
|
+
for warning in validation_results["warnings"]:
|
|
55
|
+
console.print(f" [yellow]•[/yellow] {warning}")
|
|
56
|
+
|
|
57
|
+
console.print("[green]✓ All artifacts validated successfully[/green]")
|
|
58
|
+
|
|
59
|
+
if validate_only:
|
|
60
|
+
console.print(
|
|
61
|
+
"[blue]Validation complete. Skipping publish due to --validate-only flag.[/blue]"
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
console.print(f"[bold red]Validation error:[/bold red] {e}")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
|
|
69
|
+
# Prepare content for publishing
|
|
70
|
+
server_data = validation_results["validated"]["server"]
|
|
71
|
+
docs_content = validation_results["validated"].get("docs")
|
|
72
|
+
spec_data = validation_results["validated"].get("spec")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Get client and publish
|
|
76
|
+
client = get_client()
|
|
77
|
+
console.print("Publishing to registry...")
|
|
78
|
+
|
|
79
|
+
result = asyncio.run(
|
|
80
|
+
client.publish_server(
|
|
81
|
+
server_json=server_data,
|
|
82
|
+
docs_content=docs_content,
|
|
83
|
+
spec_content=spec_data,
|
|
84
|
+
tenant=tenant,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
console.print("[bold green]✓ Published successfully![/bold green]")
|
|
89
|
+
|
|
90
|
+
# Show publish result details
|
|
91
|
+
if "server_id" in result:
|
|
92
|
+
console.print(f"Server ID: [bold]{result['server_id']}[/bold]")
|
|
93
|
+
if "version" in result:
|
|
94
|
+
console.print(f"Version: [bold]{result['version']}[/bold]")
|
|
95
|
+
if "url" in result:
|
|
96
|
+
console.print(f"Registry URL: [link]{result['url']}[/link]")
|
|
97
|
+
|
|
98
|
+
# Show what was published
|
|
99
|
+
console.print("\nPublished artifacts:")
|
|
100
|
+
console.print(" [green]•[/green] server.json")
|
|
101
|
+
if docs_content:
|
|
102
|
+
console.print(" [green]•[/green] docs.md")
|
|
103
|
+
if spec_data:
|
|
104
|
+
console.print(" [green]•[/green] OpenAPI specification")
|
|
105
|
+
|
|
106
|
+
except ValueError as e:
|
|
107
|
+
console.print(f"[bold red]Publish failed:[/bold red] {e}")
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
console.print(f"[bold red]Unexpected error:[/bold red] {e}")
|
|
111
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Query commands for searching and inspecting registry servers."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from ..client import get_client
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
FAVORITE_SCOPES = {"effective", "system", "tenant", "user", "true"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def search_servers(
|
|
19
|
+
query: Annotated[str, typer.Argument(help="Search term, e.g. redis or github")],
|
|
20
|
+
sort: Annotated[
|
|
21
|
+
Optional[str],
|
|
22
|
+
typer.Option("--sort", help="Sort order supported by the Registry API."),
|
|
23
|
+
] = None,
|
|
24
|
+
tenant: Annotated[
|
|
25
|
+
Optional[str], typer.Option("--tenant", help="Tenant context")
|
|
26
|
+
] = None,
|
|
27
|
+
limit: Annotated[
|
|
28
|
+
int, typer.Option("--limit", help="Maximum number of servers to return")
|
|
29
|
+
] = 10,
|
|
30
|
+
cursor: Annotated[
|
|
31
|
+
Optional[str], typer.Option("--cursor", help="Pagination cursor")
|
|
32
|
+
] = None,
|
|
33
|
+
show_cursor: Annotated[
|
|
34
|
+
bool,
|
|
35
|
+
typer.Option(
|
|
36
|
+
"--show-cursor",
|
|
37
|
+
help="Print the raw next-page cursor for scripted pagination.",
|
|
38
|
+
),
|
|
39
|
+
] = False,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Search MCP servers in the LightNow Registry."""
|
|
42
|
+
console.print(f"[bold blue]Searching MCP servers for '{query}'...[/bold blue]")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
client = get_client()
|
|
46
|
+
result = asyncio.run(
|
|
47
|
+
client.list_servers(
|
|
48
|
+
search=query,
|
|
49
|
+
sort=sort,
|
|
50
|
+
tenant=tenant,
|
|
51
|
+
limit=limit,
|
|
52
|
+
cursor=cursor,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
render_server_results(
|
|
56
|
+
result,
|
|
57
|
+
empty_message="No matching servers found.",
|
|
58
|
+
show_cursor=show_cursor,
|
|
59
|
+
)
|
|
60
|
+
except ValueError as e:
|
|
61
|
+
console.print(f"[bold red]Search failed:[/bold red] {e}")
|
|
62
|
+
raise typer.Exit(1)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
console.print(f"[bold red]Unexpected error:[/bold red] {e}")
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def favorite_servers(
|
|
69
|
+
scope: Annotated[
|
|
70
|
+
str,
|
|
71
|
+
typer.Option(
|
|
72
|
+
"--scope",
|
|
73
|
+
help="Favorite scope: effective, user, tenant, system, or true.",
|
|
74
|
+
),
|
|
75
|
+
] = "user",
|
|
76
|
+
sort: Annotated[
|
|
77
|
+
Optional[str],
|
|
78
|
+
typer.Option("--sort", help="Sort order supported by the Registry API."),
|
|
79
|
+
] = None,
|
|
80
|
+
tenant: Annotated[
|
|
81
|
+
Optional[str], typer.Option("--tenant", help="Tenant context")
|
|
82
|
+
] = None,
|
|
83
|
+
limit: Annotated[
|
|
84
|
+
int, typer.Option("--limit", help="Maximum number of servers to return")
|
|
85
|
+
] = 10,
|
|
86
|
+
cursor: Annotated[
|
|
87
|
+
Optional[str], typer.Option("--cursor", help="Pagination cursor")
|
|
88
|
+
] = None,
|
|
89
|
+
show_cursor: Annotated[
|
|
90
|
+
bool,
|
|
91
|
+
typer.Option(
|
|
92
|
+
"--show-cursor",
|
|
93
|
+
help="Print the raw next-page cursor for scripted pagination.",
|
|
94
|
+
),
|
|
95
|
+
] = False,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Show MCP servers favorited for the selected context."""
|
|
98
|
+
normalized_scope = scope.strip().lower()
|
|
99
|
+
if normalized_scope not in FAVORITE_SCOPES:
|
|
100
|
+
console.print(
|
|
101
|
+
"[bold red]Favorites failed:[/bold red] "
|
|
102
|
+
"scope must be effective, user, tenant, system, or true."
|
|
103
|
+
)
|
|
104
|
+
raise typer.Exit(2)
|
|
105
|
+
|
|
106
|
+
console.print(
|
|
107
|
+
f"[bold blue]Fetching {normalized_scope} favorite MCP servers...[/bold blue]"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
client = get_client()
|
|
112
|
+
result = asyncio.run(
|
|
113
|
+
client.list_servers(
|
|
114
|
+
favorites=normalized_scope,
|
|
115
|
+
sort=sort,
|
|
116
|
+
tenant=tenant,
|
|
117
|
+
limit=limit,
|
|
118
|
+
cursor=cursor,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
render_server_results(
|
|
122
|
+
result,
|
|
123
|
+
empty_message="No favorite servers found.",
|
|
124
|
+
show_cursor=show_cursor,
|
|
125
|
+
)
|
|
126
|
+
except ValueError as e:
|
|
127
|
+
console.print(f"[bold red]Favorites failed:[/bold red] {e}")
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
console.print(f"[bold red]Unexpected error:[/bold red] {e}")
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_server_results(
|
|
135
|
+
result: dict[str, Any],
|
|
136
|
+
*,
|
|
137
|
+
empty_message: str,
|
|
138
|
+
show_cursor: bool,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Render Registry server list responses."""
|
|
141
|
+
servers = result.get("servers", [])
|
|
142
|
+
if not isinstance(servers, list):
|
|
143
|
+
raise ValueError("Registry API response did not include a server list.")
|
|
144
|
+
|
|
145
|
+
if not servers:
|
|
146
|
+
console.print(f"[yellow]{empty_message}[/yellow]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
table = Table(title="MCP Servers")
|
|
150
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
151
|
+
table.add_column("Version", style="green", no_wrap=True)
|
|
152
|
+
table.add_column("Title", style="white")
|
|
153
|
+
table.add_column("Description", style="dim", max_width=60)
|
|
154
|
+
|
|
155
|
+
for entry in servers:
|
|
156
|
+
server = server_payload(entry)
|
|
157
|
+
name = string_value(server.get("name"), "N/A")
|
|
158
|
+
version = string_value(server.get("version"), "N/A")
|
|
159
|
+
title = string_value(server.get("title"), "")
|
|
160
|
+
description = string_value(server.get("description"), "")
|
|
161
|
+
if len(description) > 60:
|
|
162
|
+
description = description[:57] + "..."
|
|
163
|
+
|
|
164
|
+
table.add_row(name, version, title, description)
|
|
165
|
+
|
|
166
|
+
console.print(table)
|
|
167
|
+
metadata = result.get("metadata", {})
|
|
168
|
+
next_cursor = metadata.get("nextCursor") if isinstance(metadata, dict) else None
|
|
169
|
+
console.print(f"\nShowing {len(servers)} server(s)")
|
|
170
|
+
if isinstance(next_cursor, str) and next_cursor:
|
|
171
|
+
if show_cursor:
|
|
172
|
+
console.print(f"Next cursor: {next_cursor}")
|
|
173
|
+
else:
|
|
174
|
+
console.print(
|
|
175
|
+
"More results are available. Use a higher --limit, or pass "
|
|
176
|
+
"--show-cursor for scripted pagination."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def server_payload(entry: Any) -> dict[str, Any]:
|
|
181
|
+
"""Return the ServerJSON payload from a list item."""
|
|
182
|
+
if not isinstance(entry, dict):
|
|
183
|
+
return {}
|
|
184
|
+
nested = entry.get("server")
|
|
185
|
+
if isinstance(nested, dict):
|
|
186
|
+
return nested
|
|
187
|
+
return entry
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def string_value(value: Any, default: str) -> str:
|
|
191
|
+
"""Return a display-safe string."""
|
|
192
|
+
return value if isinstance(value, str) and value else default
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def server_info(
|
|
196
|
+
server_id: Annotated[str, typer.Argument(help="Server ID to get information for")],
|
|
197
|
+
version: Annotated[
|
|
198
|
+
Optional[str], typer.Option("--version", help="Specific version to retrieve")
|
|
199
|
+
] = None,
|
|
200
|
+
tenant: Annotated[
|
|
201
|
+
Optional[str], typer.Option("--tenant", help="Tenant context")
|
|
202
|
+
] = None,
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Get detailed information about a specific MCP server."""
|
|
205
|
+
console.print(
|
|
206
|
+
f"[bold blue]Fetching server information for '{server_id}'...[/bold blue]"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
client = get_client()
|
|
211
|
+
result = asyncio.run(
|
|
212
|
+
client.get_server_info(server_id=server_id, version=version, tenant=tenant)
|
|
213
|
+
)
|
|
214
|
+
payload = server_payload(result)
|
|
215
|
+
|
|
216
|
+
console.print(f"\n[bold cyan]Server: {payload.get('name', 'N/A')}[/bold cyan]")
|
|
217
|
+
|
|
218
|
+
if "version" in payload:
|
|
219
|
+
console.print(f"Version: [green]{payload['version']}[/green]")
|
|
220
|
+
if "title" in payload:
|
|
221
|
+
console.print(f"Title: {payload['title']}")
|
|
222
|
+
if "description" in payload:
|
|
223
|
+
console.print(f"Description: {payload['description']}")
|
|
224
|
+
|
|
225
|
+
repository = payload.get("repository")
|
|
226
|
+
if isinstance(repository, dict) and repository.get("url"):
|
|
227
|
+
console.print(f"Repository: [link]{repository['url']}[/link]")
|
|
228
|
+
|
|
229
|
+
packages = payload.get("packages")
|
|
230
|
+
if isinstance(packages, list) and packages:
|
|
231
|
+
console.print("\n[bold]Packages:[/bold]")
|
|
232
|
+
for package in packages[:5]:
|
|
233
|
+
if not isinstance(package, dict):
|
|
234
|
+
continue
|
|
235
|
+
identifier = package.get("identifier", "N/A")
|
|
236
|
+
registry_type = package.get("registryType", "package")
|
|
237
|
+
console.print(f" • [green]{registry_type}[/green] {identifier}")
|
|
238
|
+
|
|
239
|
+
env_vars = package.get("environmentVariables")
|
|
240
|
+
if isinstance(env_vars, list) and env_vars:
|
|
241
|
+
for env_var in env_vars:
|
|
242
|
+
if not isinstance(env_var, dict):
|
|
243
|
+
continue
|
|
244
|
+
required = (
|
|
245
|
+
"required" if env_var.get("isRequired") else "optional"
|
|
246
|
+
)
|
|
247
|
+
secret = ", secret" if env_var.get("isSecret") else ""
|
|
248
|
+
name = env_var.get("name", "N/A")
|
|
249
|
+
description = env_var.get("description")
|
|
250
|
+
suffix = f" - {description}" if description else ""
|
|
251
|
+
console.print(f" env {name} ({required}{secret}){suffix}")
|
|
252
|
+
|
|
253
|
+
remotes = payload.get("remotes")
|
|
254
|
+
if isinstance(remotes, list) and remotes:
|
|
255
|
+
console.print("\n[bold]Remote transports:[/bold]")
|
|
256
|
+
for remote in remotes[:5]:
|
|
257
|
+
if not isinstance(remote, dict):
|
|
258
|
+
continue
|
|
259
|
+
transport_type = remote.get("type", "remote")
|
|
260
|
+
url = remote.get("url", "N/A")
|
|
261
|
+
console.print(f" • [green]{transport_type}[/green] {url}")
|
|
262
|
+
|
|
263
|
+
except ValueError as e:
|
|
264
|
+
console.print(f"[bold red]Query failed:[/bold red] {e}")
|
|
265
|
+
raise typer.Exit(1)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
console.print(f"[bold red]Unexpected error:[/bold red] {e}")
|
|
268
|
+
raise typer.Exit(1)
|