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.
@@ -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)