lightnow-cli 1.0.1__tar.gz → 1.0.2__tar.gz

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 (39) hide show
  1. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/PKG-INFO +1 -1
  2. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/__init__.py +1 -1
  3. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/client.py +2 -4
  4. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/pyproject.toml +1 -1
  5. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/test_cli.py +98 -1
  6. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/test_client.py +30 -0
  7. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/test_integrations.py +78 -0
  8. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/.github/workflows/ci.yml +0 -0
  9. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/.github/workflows/release.yml +0 -0
  10. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/.gitignore +0 -0
  11. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/CHANGELOG.md +0 -0
  12. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/CONTRIBUTING.md +0 -0
  13. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/LICENSE +0 -0
  14. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/Makefile +0 -0
  15. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/README.md +0 -0
  16. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/SECURITY.md +0 -0
  17. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/docs/development.md +0 -0
  18. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/docs/release.md +0 -0
  19. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/examples/README.md +0 -0
  20. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/examples/docs.md +0 -0
  21. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/examples/openapi.json +0 -0
  22. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/examples/server.json +0 -0
  23. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/authenticated_http.py +0 -0
  24. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/commands/__init__.py +0 -0
  25. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/commands/auth.py +0 -0
  26. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/commands/integrations.py +0 -0
  27. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/commands/publish.py +0 -0
  28. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/commands/query.py +0 -0
  29. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/commands/runner.py +0 -0
  30. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/commands/validate.py +0 -0
  31. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/config.py +0 -0
  32. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/main.py +0 -0
  33. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/tls.py +0 -0
  34. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/lightnow_cli/validation.py +0 -0
  35. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/__init__.py +0 -0
  36. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/test_auth_flow.py +0 -0
  37. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/test_config.py +0 -0
  38. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/test_tls.py +0 -0
  39. {lightnow_cli-1.0.1 → lightnow_cli-1.0.2}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lightnow-cli
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows
5
5
  Project-URL: Homepage, https://github.com/lightnow-ai/lightnow-cli
6
6
  Project-URL: Repository, https://github.com/lightnow-ai/lightnow-cli
@@ -1,3 +1,3 @@
1
1
  """LightNow CLI - A command-line tool for managing MCP servers."""
2
2
 
3
- __version__ = "1.0.1"
3
+ __version__ = "1.0.2"
@@ -151,8 +151,6 @@ class MCPRegistryClient:
151
151
  params["favorites"] = favorites
152
152
  if sort:
153
153
  params["sort"] = sort
154
- if tenant:
155
- params["tenant"] = tenant
156
154
  if cursor:
157
155
  params["cursor"] = cursor
158
156
 
@@ -163,6 +161,7 @@ class MCPRegistryClient:
163
161
  params=params,
164
162
  headers=self._get_headers(),
165
163
  token=self.token,
164
+ tenant=tenant,
166
165
  allow_refresh=self._refresh_on_unauthorized,
167
166
  timeout=30.0,
168
167
  )
@@ -191,8 +190,6 @@ class MCPRegistryClient:
191
190
  ) -> Dict[str, Any]:
192
191
  """Get detailed information about a specific server."""
193
192
  params: Dict[str, Any] = {}
194
- if tenant:
195
- params["tenant"] = tenant
196
193
 
197
194
  encoded_server_id = quote(server_id, safe="")
198
195
  if version:
@@ -209,6 +206,7 @@ class MCPRegistryClient:
209
206
  params=params,
210
207
  headers=self._get_headers(),
211
208
  token=self.token,
209
+ tenant=tenant,
212
210
  allow_refresh=self._refresh_on_unauthorized,
213
211
  timeout=30.0,
214
212
  )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lightnow-cli"
7
- version = "1.0.1"
7
+ version = "1.0.2"
8
8
  description = "Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows"
9
9
  authors = [{name = "LightNow Team"}]
10
10
  license = {text = "Apache-2.0"}
@@ -51,7 +51,7 @@ def test_cli_version(runner):
51
51
  """Test CLI version command."""
52
52
  result = runner.invoke(app, ["--version"])
53
53
  assert result.exit_code == 0
54
- assert "LightNow CLI 1.0.1" in result.stdout
54
+ assert "LightNow CLI 1.0.2" in result.stdout
55
55
 
56
56
 
57
57
  def test_cli_help(runner):
@@ -175,6 +175,103 @@ def test_run_command_starts_profile_server(runner):
175
175
  assert run_process.call_args.kwargs["env"]["SONARQUBE_TOKEN"] == "secret"
176
176
 
177
177
 
178
+ def test_run_command_passes_tenant_to_profile_and_context_requests(runner):
179
+ """The local runner keeps organization context for lookup and secret context."""
180
+ with (
181
+ patch(
182
+ "lightnow_cli.commands.runner.require_access_token",
183
+ return_value="token",
184
+ ),
185
+ patch(
186
+ "lightnow_cli.commands.runner.resolve_profile_server",
187
+ return_value={
188
+ "alias": "sonarqube",
189
+ "server_name": "io.github.sonarsource/sonarqube-mcp-server",
190
+ "version": "1.2.3",
191
+ "status": "linked",
192
+ },
193
+ ) as resolve,
194
+ patch(
195
+ "lightnow_cli.commands.runner.fetch_runtime_context",
196
+ return_value={"probe_request": {"transport": "stdio", "stdio": {}}},
197
+ ) as fetch_context,
198
+ patch(
199
+ "lightnow_cli.commands.runner.launch_config_from_context",
200
+ return_value=("python", ["-m", "server"], {}, None),
201
+ ),
202
+ patch(
203
+ "lightnow_cli.commands.runner.subprocess.run",
204
+ return_value=MagicMock(returncode=0),
205
+ ),
206
+ ):
207
+ result = runner.invoke(
208
+ app,
209
+ [
210
+ "run",
211
+ "--server",
212
+ "sonarqube",
213
+ "--profile",
214
+ "default",
215
+ "--tenant",
216
+ "acme",
217
+ "--api-url",
218
+ "https://registry-api.lightnow.local/v0.1",
219
+ ],
220
+ )
221
+
222
+ assert result.exit_code == 0
223
+ assert resolve.call_args.kwargs["tenant"] == "acme"
224
+ assert fetch_context.call_args.kwargs["tenant"] == "acme"
225
+
226
+
227
+ def test_search_command_passes_tenant_context(runner):
228
+ """Search supports organization context from the public CLI surface."""
229
+ fake_client = MagicMock()
230
+ fake_client.list_servers = AsyncMock(return_value={"servers": []})
231
+
232
+ with patch("lightnow_cli.commands.query.get_client", return_value=fake_client):
233
+ result = runner.invoke(
234
+ app, ["search", "redis", "--tenant", "acme", "--limit", "5"]
235
+ )
236
+
237
+ assert result.exit_code == 0
238
+ assert fake_client.list_servers.call_args.kwargs["tenant"] == "acme"
239
+
240
+
241
+ def test_favorites_command_passes_tenant_context(runner):
242
+ """Favorites supports organization-scoped favorites from the CLI."""
243
+ fake_client = MagicMock()
244
+ fake_client.list_servers = AsyncMock(return_value={"servers": []})
245
+
246
+ with patch("lightnow_cli.commands.query.get_client", return_value=fake_client):
247
+ result = runner.invoke(
248
+ app, ["favorites", "--scope", "tenant", "--tenant", "acme"]
249
+ )
250
+
251
+ assert result.exit_code == 0
252
+ assert fake_client.list_servers.call_args.kwargs["favorites"] == "tenant"
253
+ assert fake_client.list_servers.call_args.kwargs["tenant"] == "acme"
254
+
255
+
256
+ def test_info_command_passes_tenant_context(runner):
257
+ """Server info supports organization context from the public CLI surface."""
258
+ fake_client = MagicMock()
259
+ fake_client.get_server_info = AsyncMock(
260
+ return_value={
261
+ "name": "io.github.test/server",
262
+ "version": "1.0.0",
263
+ }
264
+ )
265
+
266
+ with patch("lightnow_cli.commands.query.get_client", return_value=fake_client):
267
+ result = runner.invoke(
268
+ app, ["info", "io.github.test/server", "--tenant", "acme"]
269
+ )
270
+
271
+ assert result.exit_code == 0
272
+ assert fake_client.get_server_info.call_args.kwargs["tenant"] == "acme"
273
+
274
+
178
275
  def test_run_command_reports_expired_token(runner):
179
276
  """Runner commands use the shared expired-token message."""
180
277
  from lightnow_cli.commands.auth import (
@@ -221,6 +221,21 @@ async def test_client_lists_servers(monkeypatch) -> None:
221
221
  assert fake.last_params["cursor"] == "cursor-token"
222
222
 
223
223
 
224
+ @pytest.mark.asyncio
225
+ async def test_client_lists_servers_with_tenant_header(monkeypatch) -> None:
226
+ """Tenant context for list queries is sent as X-Tenant, not as a query param."""
227
+ fake = FakeHttpClient()
228
+ monkeypatch.setattr("lightnow_cli.client.httpx.AsyncClient", lambda: fake)
229
+
230
+ client = MCPRegistryClient(
231
+ base_url="https://registry-api.lightnow.local/v0.1", token="token"
232
+ )
233
+ await client.list_servers(favorites="tenant", tenant="tenant-uuid")
234
+
235
+ assert fake.last_headers["X-Tenant"] == "tenant-uuid"
236
+ assert "tenant" not in fake.last_params
237
+
238
+
224
239
  @pytest.mark.asyncio
225
240
  async def test_client_gets_server_info(monkeypatch) -> None:
226
241
  """Server info lookup returns the decoded registry payload."""
@@ -236,6 +251,21 @@ async def test_client_gets_server_info(monkeypatch) -> None:
236
251
  assert fake.last_url.endswith("/servers/github/versions/1.0.0")
237
252
 
238
253
 
254
+ @pytest.mark.asyncio
255
+ async def test_client_gets_server_info_with_tenant_header(monkeypatch) -> None:
256
+ """Tenant context for server detail lookup is sent as X-Tenant."""
257
+ fake = FakeHttpClient()
258
+ monkeypatch.setattr("lightnow_cli.client.httpx.AsyncClient", lambda: fake)
259
+
260
+ client = MCPRegistryClient(
261
+ base_url="https://registry-api.lightnow.local/v0.1", token="token"
262
+ )
263
+ await client.get_server_info("github", version="1.0.0", tenant="tenant-uuid")
264
+
265
+ assert fake.last_headers["X-Tenant"] == "tenant-uuid"
266
+ assert "tenant" not in fake.last_params
267
+
268
+
239
269
  @pytest.mark.asyncio
240
270
  async def test_client_encodes_registry_server_names(monkeypatch) -> None:
241
271
  """Registry server names with slashes are encoded in path parameters."""
@@ -173,6 +173,39 @@ def test_sync_defaults_to_plaintext_export_mode() -> None:
173
173
  assert fetch.call_args.kwargs["secret_mode"] == "plaintext"
174
174
 
175
175
 
176
+ def test_sync_passes_tenant_to_export_request() -> None:
177
+ """Client export sync uses the selected organization context."""
178
+ runner = CliRunner()
179
+ with tempfile.TemporaryDirectory() as tmp:
180
+ target = Path(tmp) / "config.toml"
181
+ with (
182
+ patch(
183
+ "lightnow_cli.commands.integrations.require_access_token",
184
+ return_value="token",
185
+ ),
186
+ patch(
187
+ "lightnow_cli.commands.integrations.fetch_export",
188
+ return_value='[mcp_servers.github]\ncommand = "docker"\n',
189
+ ) as fetch,
190
+ ):
191
+ result = runner.invoke(
192
+ app,
193
+ [
194
+ "sync",
195
+ "--client",
196
+ "codex",
197
+ "--tenant",
198
+ "acme",
199
+ "--config-path",
200
+ str(target),
201
+ ],
202
+ input="y\n",
203
+ )
204
+
205
+ assert result.exit_code == 0
206
+ assert fetch.call_args.kwargs["tenant"] == "acme"
207
+
208
+
176
209
  def test_sync_json_writes_manifest_and_preserves_user_config() -> None:
177
210
  """JSON sync stores the LightNow-owned aliases in a sibling manifest."""
178
211
  runner = CliRunner()
@@ -509,6 +542,51 @@ def test_sync_runner_dry_run_does_not_fetch_plaintext_export() -> None:
509
542
  assert not target.exists()
510
543
 
511
544
 
545
+ def test_sync_runner_passes_tenant_to_profile_server_lookup() -> None:
546
+ """Runner sync builds organization-aware client commands from tenant profiles."""
547
+ runner = CliRunner()
548
+ with tempfile.TemporaryDirectory() as tmp:
549
+ target = Path(tmp) / "config.toml"
550
+ with (
551
+ patch(
552
+ "lightnow_cli.commands.integrations.require_access_token",
553
+ return_value="token",
554
+ ),
555
+ patch(
556
+ "lightnow_cli.commands.integrations.fetch_profile_servers",
557
+ return_value={
558
+ "servers": [
559
+ {
560
+ "alias": "sonarqube",
561
+ "server_name": "io.github.sonarsource/sonarqube-mcp-server",
562
+ "version": "1.2.3",
563
+ "status": "linked",
564
+ }
565
+ ]
566
+ },
567
+ ) as fetch_profile,
568
+ ):
569
+ result = runner.invoke(
570
+ app,
571
+ [
572
+ "sync",
573
+ "--client",
574
+ "codex",
575
+ "--tenant",
576
+ "acme",
577
+ "--runner",
578
+ "--config-path",
579
+ str(target),
580
+ "--dry-run",
581
+ ],
582
+ )
583
+
584
+ assert result.exit_code == 0
585
+ assert fetch_profile.call_args.kwargs["tenant"] == "acme"
586
+ assert "--tenant" in result.stdout
587
+ assert "acme" in result.stdout
588
+
589
+
512
590
  def test_fetch_runtime_context_requests_local_runner_secret_context() -> None:
513
591
  """The runner asks for secret context with consumer=local-runner."""
514
592
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes