lightnow-cli 1.6.1__tar.gz → 1.7.1__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 (57) hide show
  1. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.github/workflows/ci.yml +2 -2
  2. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.github/workflows/release.yml +3 -3
  3. lightnow_cli-1.7.1/.release-please-manifest.json +3 -0
  4. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/CHANGELOG.md +14 -0
  5. lightnow_cli-1.7.1/PKG-INFO +138 -0
  6. lightnow_cli-1.7.1/README.md +90 -0
  7. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/__init__.py +1 -1
  8. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/integrations.py +142 -12
  9. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/pyproject.toml +1 -1
  10. lightnow_cli-1.7.1/tests/test_import_preconditions.py +197 -0
  11. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_integrations.py +305 -1
  12. lightnow_cli-1.6.1/.release-please-manifest.json +0 -3
  13. lightnow_cli-1.6.1/PKG-INFO +0 -344
  14. lightnow_cli-1.6.1/README.md +0 -296
  15. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.github/workflows/release-please.yml +0 -0
  16. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.gitignore +0 -0
  17. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/CONTRIBUTING.md +0 -0
  18. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/LICENSE +0 -0
  19. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/Makefile +0 -0
  20. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/SECURITY.md +0 -0
  21. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/ci/create_client_artifact.py +0 -0
  22. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/ci/test_client_artifact.py +0 -0
  23. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/docs/development.md +0 -0
  24. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/docs/release.md +0 -0
  25. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/README.md +0 -0
  26. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/docs.md +0 -0
  27. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/openapi.json +0 -0
  28. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/server.json +0 -0
  29. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/authenticated_http.py +0 -0
  30. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/client.py +0 -0
  31. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/__init__.py +0 -0
  32. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/auth.py +0 -0
  33. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/context.py +0 -0
  34. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/publish.py +0 -0
  35. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/query.py +0 -0
  36. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/runner.py +0 -0
  37. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/updates.py +0 -0
  38. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/validate.py +0 -0
  39. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/config.py +0 -0
  40. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/main.py +0 -0
  41. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/target.py +0 -0
  42. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/tls.py +0 -0
  43. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/updates.py +0 -0
  44. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/validation.py +0 -0
  45. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/release-please-config.json +0 -0
  46. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/renovate.json +0 -0
  47. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/__init__.py +0 -0
  48. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_auth_flow.py +0 -0
  49. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_authenticated_http.py +0 -0
  50. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_cli.py +0 -0
  51. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_client.py +0 -0
  52. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_config.py +0 -0
  53. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_connection_target.py +0 -0
  54. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_context.py +0 -0
  55. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_tls.py +0 -0
  56. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_updates.py +0 -0
  57. {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_validation.py +0 -0
@@ -26,7 +26,7 @@ jobs:
26
26
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
27
27
 
28
28
  - name: Set up Python
29
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
29
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
30
30
  with:
31
31
  python-version: ${{ matrix.python-version }}
32
32
  cache: pip
@@ -62,7 +62,7 @@ jobs:
62
62
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
63
63
 
64
64
  - name: Set up Python
65
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
65
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
66
66
  with:
67
67
  python-version: "3.12"
68
68
  cache: pip
@@ -18,7 +18,7 @@ jobs:
18
18
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
19
19
 
20
20
  - name: Set up Python
21
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
21
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
22
22
  with:
23
23
  python-version: "3.12"
24
24
  cache: pip
@@ -39,7 +39,7 @@ jobs:
39
39
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
40
40
 
41
41
  - name: Set up Python
42
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
42
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
43
43
  with:
44
44
  python-version: "3.12"
45
45
  cache: pip
@@ -95,7 +95,7 @@ jobs:
95
95
  path: dist
96
96
 
97
97
  - name: Publish distributions to PyPI
98
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
98
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
99
99
 
100
100
  update-homebrew:
101
101
  name: Update Homebrew formula
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.7.1"
3
+ }
@@ -4,6 +4,20 @@ All notable changes to the LightNow CLI are documented here.
4
4
 
5
5
  This project follows semantic versioning.
6
6
 
7
+ ## [1.7.1](https://github.com/lightnow-ai/lightnow-cli/compare/v1.7.0...v1.7.1) (2026-11-14)
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * honor profile revisions when importing MCP config ([#50](https://github.com/lightnow-ai/lightnow-cli/issues/50)) ([343ab0d](https://github.com/lightnow-ai/lightnow-cli/commit/343ab0d545c19d3c9a44fb9097ed06e5f00ca3fb))
13
+
14
+ ## [1.7.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.1...v1.7.0) (2026-11-13)
15
+
16
+
17
+ ### Features
18
+
19
+ * support LibreChat local proxy configuration ([#47](https://github.com/lightnow-ai/lightnow-cli/issues/47)) ([fffb3ca](https://github.com/lightnow-ai/lightnow-cli/commit/fffb3ca2ac8c87f08e9938bfcb54e3952fcb23f7))
20
+
7
21
  ## [1.6.1](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.0...v1.6.1) (2026-11-09)
8
22
 
9
23
 
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: lightnow-cli
3
+ Version: 1.7.1
4
+ Summary: Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows
5
+ Project-URL: Homepage, https://github.com/lightnow-ai/lightnow-cli
6
+ Project-URL: Repository, https://github.com/lightnow-ai/lightnow-cli
7
+ Author: LightNow Team
8
+ License: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: cli,lightnow,mcp,model-context-protocol,registry
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: filelock>=3.16.1
22
+ Requires-Dist: httpx>=0.24.0
23
+ Requires-Dist: jsonschema>=4.17.0
24
+ Requires-Dist: packaging>=24.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: pyjwt>=2.8.0
27
+ Requires-Dist: pyyaml>=6.0.0
28
+ Requires-Dist: rich>=13.0.0
29
+ Requires-Dist: truststore>=0.10.0
30
+ Requires-Dist: typer>=0.9.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: black>=23.0.0; extra == 'dev'
33
+ Requires-Dist: build>=1.2.0; extra == 'dev'
34
+ Requires-Dist: flake8>=6.0.0; extra == 'dev'
35
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
36
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
37
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
38
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
39
+ Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
40
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
41
+ Requires-Dist: responses>=0.23.0; extra == 'dev'
42
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
43
+ Requires-Dist: types-jsonschema>=4.25.0; extra == 'dev'
44
+ Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
45
+ Provides-Extra: keyring
46
+ Requires-Dist: keyring>=25.6.0; extra == 'keyring'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # LightNow CLI
50
+
51
+ [![PyPI](https://img.shields.io/pypi/v/lightnow-cli.svg)](https://pypi.org/project/lightnow-cli/)
52
+
53
+ Find MCP servers and connect your AI assistants to the tools you manage in LightNow.
54
+
55
+ The CLI signs you in and configures your clients. [LightNow Proxy](https://github.com/lightnow-ai/lightnow-proxy)
56
+ runs their connections to the servers in your Runtime profiles.
57
+
58
+ ## Get started
59
+
60
+ You need a [LightNow account](https://lightnow.ai/), an AI client and a Runtime
61
+ profile with the server connections you want to use.
62
+
63
+ ### 1. Install the CLI and proxy
64
+
65
+ With Homebrew:
66
+
67
+ ```sh
68
+ brew tap lightnow-ai/tap
69
+ brew install lightnow-cli lightnow-proxy
70
+ ```
71
+
72
+ Or with [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.11 or later:
73
+
74
+ ```sh
75
+ uv tool install lightnow-cli
76
+ uv tool install lightnow-proxy
77
+ ```
78
+
79
+ [pipx](https://pipx.pypa.io/stable/installation/) is also supported.
80
+
81
+ ### 2. Sign in and connect a client
82
+
83
+ If your client already has MCP servers, [review the import options](https://docs.lightnow.ai/how-to/import-existing-mcp-config)
84
+ before syncing.
85
+
86
+ ```sh
87
+ lightnow login
88
+ lightnow sync --client codex --profile default --local-proxy
89
+ ```
90
+
91
+ Replace `default` with your profile's name. Restart the client and check its MCP
92
+ tools list. The CLI supports Codex, Claude Code, Claude Desktop, Cursor, VS Code,
93
+ Antigravity, Gemini CLI and LibreChat.
94
+
95
+ See [Connect AI assistants](https://docs.lightnow.ai/getting-started/sync-mcp-clients)
96
+ for each client's setup and configuration paths.
97
+
98
+ For LibreChat, run the CLI and proxy on its backend and select its configuration:
99
+
100
+ ```sh
101
+ lightnow sync --client librechat --profile default --local-proxy --config-path /path/to/librechat.yaml
102
+ ```
103
+
104
+ LibreChat users with access to this connection share the backend account's
105
+ LightNow permissions. Reload LibreChat after changing its configuration.
106
+
107
+ ### 3. Check the connection
108
+
109
+ ```sh
110
+ lightnow status
111
+ lightnow config-status --client codex
112
+ lightnow-proxy --config ~/.lightnow/lightnow-proxy/codex.yaml --health
113
+ ```
114
+
115
+ For help, see [Troubleshoot the connection](https://docs.lightnow.ai/how-to/debug-local-proxy).
116
+
117
+ ## Useful commands
118
+
119
+ | Task | Command |
120
+ | --- | --- |
121
+ | Find servers | `lightnow search sonarqube` |
122
+ | View your favorites | `lightnow favorites` |
123
+ | Use organization-managed client settings | `lightnow sync --client codex --from-settings` |
124
+ | Check for updates | `lightnow update --check` |
125
+ | Update the CLI and proxy | `lightnow update` |
126
+
127
+ Use `lightnow --help` or `lightnow <command> --help` for available options.
128
+ For profiles, organization connections and other workflows, see the
129
+ [CLI reference](https://docs.lightnow.ai/reference/cli).
130
+
131
+ ## Contributing
132
+
133
+ See [development setup](docs/development.md), [contributing](CONTRIBUTING.md),
134
+ [release process](docs/release.md) and [security policy](SECURITY.md).
135
+
136
+ ## License
137
+
138
+ [Apache License 2.0](LICENSE).
@@ -0,0 +1,90 @@
1
+ # LightNow CLI
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/lightnow-cli.svg)](https://pypi.org/project/lightnow-cli/)
4
+
5
+ Find MCP servers and connect your AI assistants to the tools you manage in LightNow.
6
+
7
+ The CLI signs you in and configures your clients. [LightNow Proxy](https://github.com/lightnow-ai/lightnow-proxy)
8
+ runs their connections to the servers in your Runtime profiles.
9
+
10
+ ## Get started
11
+
12
+ You need a [LightNow account](https://lightnow.ai/), an AI client and a Runtime
13
+ profile with the server connections you want to use.
14
+
15
+ ### 1. Install the CLI and proxy
16
+
17
+ With Homebrew:
18
+
19
+ ```sh
20
+ brew tap lightnow-ai/tap
21
+ brew install lightnow-cli lightnow-proxy
22
+ ```
23
+
24
+ Or with [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.11 or later:
25
+
26
+ ```sh
27
+ uv tool install lightnow-cli
28
+ uv tool install lightnow-proxy
29
+ ```
30
+
31
+ [pipx](https://pipx.pypa.io/stable/installation/) is also supported.
32
+
33
+ ### 2. Sign in and connect a client
34
+
35
+ If your client already has MCP servers, [review the import options](https://docs.lightnow.ai/how-to/import-existing-mcp-config)
36
+ before syncing.
37
+
38
+ ```sh
39
+ lightnow login
40
+ lightnow sync --client codex --profile default --local-proxy
41
+ ```
42
+
43
+ Replace `default` with your profile's name. Restart the client and check its MCP
44
+ tools list. The CLI supports Codex, Claude Code, Claude Desktop, Cursor, VS Code,
45
+ Antigravity, Gemini CLI and LibreChat.
46
+
47
+ See [Connect AI assistants](https://docs.lightnow.ai/getting-started/sync-mcp-clients)
48
+ for each client's setup and configuration paths.
49
+
50
+ For LibreChat, run the CLI and proxy on its backend and select its configuration:
51
+
52
+ ```sh
53
+ lightnow sync --client librechat --profile default --local-proxy --config-path /path/to/librechat.yaml
54
+ ```
55
+
56
+ LibreChat users with access to this connection share the backend account's
57
+ LightNow permissions. Reload LibreChat after changing its configuration.
58
+
59
+ ### 3. Check the connection
60
+
61
+ ```sh
62
+ lightnow status
63
+ lightnow config-status --client codex
64
+ lightnow-proxy --config ~/.lightnow/lightnow-proxy/codex.yaml --health
65
+ ```
66
+
67
+ For help, see [Troubleshoot the connection](https://docs.lightnow.ai/how-to/debug-local-proxy).
68
+
69
+ ## Useful commands
70
+
71
+ | Task | Command |
72
+ | --- | --- |
73
+ | Find servers | `lightnow search sonarqube` |
74
+ | View your favorites | `lightnow favorites` |
75
+ | Use organization-managed client settings | `lightnow sync --client codex --from-settings` |
76
+ | Check for updates | `lightnow update --check` |
77
+ | Update the CLI and proxy | `lightnow update` |
78
+
79
+ Use `lightnow --help` or `lightnow <command> --help` for available options.
80
+ For profiles, organization connections and other workflows, see the
81
+ [CLI reference](https://docs.lightnow.ai/reference/cli).
82
+
83
+ ## Contributing
84
+
85
+ See [development setup](docs/development.md), [contributing](CONTRIBUTING.md),
86
+ [release process](docs/release.md) and [security policy](SECURITY.md).
87
+
88
+ ## License
89
+
90
+ [Apache License 2.0](LICENSE).
@@ -1,3 +1,3 @@
1
1
  """LightNow CLI - A command-line tool for managing MCP servers."""
2
2
 
3
- __version__ = "1.6.1" # x-release-please-version
3
+ __version__ = "1.7.1" # x-release-please-version
@@ -121,7 +121,7 @@ VSCODE_VIRTUAL_TOOLS_THRESHOLD = 128
121
121
  LOCAL_PROXY_STDIO_SUPPORT_MESSAGE = (
122
122
  "Local Proxy Mode stdio currently supports Codex TOML, "
123
123
  "Antigravity JSON, Claude Code JSON, Claude Desktop JSON, "
124
- "Cursor JSON, Gemini CLI JSON, and VS Code JSON only."
124
+ "Cursor JSON, Gemini CLI JSON, VS Code JSON, and LibreChat YAML only."
125
125
  )
126
126
  LOCAL_PROXY_HTTP_SUPPORT_MESSAGE = (
127
127
  "Local Proxy Mode HTTP currently supports Codex TOML only."
@@ -237,6 +237,8 @@ def local_proxy_support_error(
237
237
  ) -> Optional[str]:
238
238
  """Return why a client/format/transport combination lacks Local Proxy support."""
239
239
  if local_proxy_transport == "stdio":
240
+ if client == "librechat" and export_format == "yaml":
241
+ return None
240
242
  if client in LOCAL_PROXY_JSON_CLIENTS and export_format == "json":
241
243
  return None
242
244
  if client == "codex" and export_format == "toml":
@@ -271,12 +273,29 @@ def backup_path_for(path: Path) -> Path:
271
273
 
272
274
  def direct_server_aliases(content: str, export_format: str) -> list[str]:
273
275
  """Return non-LightNow MCP server aliases found in a client config."""
274
- if export_format not in {"json", "toml"}:
276
+ if export_format not in {"json", "toml", "yaml"}:
275
277
  return []
276
278
  try:
277
279
  entries = extract_mcp_entries(content, export_format)
278
280
  except ValueError:
279
281
  return []
282
+ if export_format == "yaml":
283
+ return sorted(
284
+ alias
285
+ for alias, entry in entries.items()
286
+ if not (
287
+ command_looks_like(
288
+ str(entry.get("command") or ""), LOCAL_PROXY_EXECUTABLE
289
+ )
290
+ or command_looks_like(
291
+ str(entry.get("command") or ""), LEGACY_LOCAL_PROXY_EXECUTABLE
292
+ )
293
+ or (
294
+ is_lightnow_connection_alias(alias)
295
+ and is_local_proxy_url(str(entry.get("url") or ""))
296
+ )
297
+ )
298
+ )
280
299
  return sorted(alias for alias in entries if alias not in LIGHTNOW_PROXY_ALIASES)
281
300
 
282
301
 
@@ -626,13 +645,18 @@ def sync(
626
645
  patch_aliases = previous_managed["aliases"]
627
646
  if local_proxy and export_format == "json":
628
647
  patch_aliases = ["LightNow" if connection == "lightnow" else connection]
629
- patched = patch_config(
630
- existing,
631
- generated,
632
- export_format,
633
- patch_aliases,
634
- previous_managed["input_ids"],
635
- )
648
+ if local_proxy and client == "librechat" and export_format == "yaml":
649
+ patched = patch_local_proxy_yaml_config(
650
+ existing, generated, remove_unmanaged_client_servers
651
+ )
652
+ else:
653
+ patched = patch_config(
654
+ existing,
655
+ generated,
656
+ export_format,
657
+ patch_aliases,
658
+ previous_managed["input_ids"],
659
+ )
636
660
  if (
637
661
  not local_proxy
638
662
  and not runner
@@ -874,7 +898,9 @@ def import_config(
874
898
  ] = "default",
875
899
  tenant: Annotated[
876
900
  Optional[str],
877
- typer.Option("--tenant", help="Tenant id or slug, sent as X-Tenant"),
901
+ typer.Option(
902
+ "--tenant", help="Organization ID; defaults to the selected context"
903
+ ),
878
904
  ] = None,
879
905
  config_path: Annotated[
880
906
  Optional[Path],
@@ -965,6 +991,12 @@ def build_local_proxy_export(
965
991
  if support_error:
966
992
  raise ValueError(support_error)
967
993
  if local_proxy_transport == "stdio":
994
+ if client == "librechat" and export_format == "yaml":
995
+ return render_local_proxy_librechat_yaml(
996
+ local_proxy_config_path
997
+ or default_local_proxy_config_path(client, connection_alias),
998
+ connection_alias,
999
+ )
968
1000
  if client in LOCAL_PROXY_MCP_SERVERS_JSON_CLIENTS and export_format == "json":
969
1001
  return render_local_proxy_mcp_servers_json(
970
1002
  local_proxy_config_path
@@ -1099,6 +1131,18 @@ def render_local_proxy_mcp_servers_json(
1099
1131
  return json.dumps(payload, indent=2) + "\n"
1100
1132
 
1101
1133
 
1134
+ def render_local_proxy_librechat_yaml(
1135
+ local_proxy_config_path: Path, connection_alias: str = "lightnow"
1136
+ ) -> str:
1137
+ """Render an operator-owned LibreChat stdio entry on its backend host."""
1138
+ payload = json.loads(
1139
+ render_local_proxy_mcp_servers_json(local_proxy_config_path, connection_alias)
1140
+ )
1141
+ for entry in payload["mcpServers"].values():
1142
+ entry["type"] = "stdio"
1143
+ return yaml.safe_dump(payload, sort_keys=False, allow_unicode=True)
1144
+
1145
+
1102
1146
  def render_local_proxy_vscode_json(
1103
1147
  local_proxy_config_path: Path, connection_alias: str = "lightnow"
1104
1148
  ) -> str:
@@ -1657,7 +1701,34 @@ def import_profile_config(
1657
1701
  ) -> dict[str, Any]:
1658
1702
  """Import rendered client config into a LightNow runtime profile."""
1659
1703
  url = f"{api_url.rstrip('/')}/integrations/import"
1704
+ headers = {"Accept": "application/json"}
1660
1705
  try:
1706
+ if not dry_run:
1707
+ current = request_with_refresh(
1708
+ "GET",
1709
+ f"{api_url.rstrip('/')}/integrations/profiles",
1710
+ headers=headers,
1711
+ token=token,
1712
+ tenant=tenant,
1713
+ timeout=30.0,
1714
+ )
1715
+ if current.status_code == 401:
1716
+ raise authentication_error_from_response(current)
1717
+ if current.status_code != 200:
1718
+ raise ValueError(f"HTTP {current.status_code}: {redact(current.text)}")
1719
+ etag = current.headers.get("ETag")
1720
+ if (
1721
+ etag is None
1722
+ or re.fullmatch(r'"[\x21\x23-\x7e\x80-\xff]*"', etag) is None
1723
+ ):
1724
+ raise ValueError(
1725
+ "Could not read the current profile version. No import was applied."
1726
+ )
1727
+ headers["If-Match"] = etag
1728
+ # The snapshot request may have refreshed an expired session.
1729
+ authorization = current.request.headers.get("Authorization", "")
1730
+ if authorization.startswith("Bearer "):
1731
+ token = authorization.removeprefix("Bearer ")
1661
1732
  response = request_with_refresh(
1662
1733
  "POST",
1663
1734
  url,
@@ -1668,7 +1739,7 @@ def import_profile_config(
1668
1739
  "profile": {"name": profile},
1669
1740
  "replace": replace,
1670
1741
  },
1671
- headers={"Accept": "application/json"},
1742
+ headers=headers,
1672
1743
  token=token,
1673
1744
  tenant=tenant,
1674
1745
  timeout=30.0,
@@ -2084,6 +2155,11 @@ def extract_mcp_entries(content: str, export_format: str) -> dict[str, dict[str,
2084
2155
  if isinstance(entry, dict):
2085
2156
  entries[str(alias)] = entry
2086
2157
  return entries
2158
+ if export_format == "yaml":
2159
+ payload = load_yaml_client_config(content)
2160
+ return {
2161
+ str(alias): entry for alias, entry in payload.get("mcpServers", {}).items()
2162
+ }
2087
2163
  raise ValueError(f"unsupported_format:{export_format}")
2088
2164
 
2089
2165
 
@@ -2106,7 +2182,7 @@ def is_local_proxy_url(value: str) -> bool:
2106
2182
  def warning_hint(code: str, client: str) -> Optional[str]:
2107
2183
  """Return a short recovery hint for a posture warning code."""
2108
2184
  sync_command = f"lightnow sync --client {client} --local-proxy"
2109
- if code.startswith("invalid_json") or code.startswith("invalid_toml"):
2185
+ if code.startswith(("invalid_json", "invalid_toml", "invalid_yaml")):
2110
2186
  return (
2111
2187
  "the config file could not be parsed; fix it or restore the "
2112
2188
  ".lightnow.bak backup, then re-run the sync."
@@ -2439,6 +2515,60 @@ def merge_local_proxy_toml_connection(
2439
2515
  return "\n\n".join(part for part in (kept, generated.strip()) if part)
2440
2516
 
2441
2517
 
2518
+ class ClientConfigSafeLoader(yaml.SafeLoader):
2519
+ """Reject duplicate YAML keys rather than silently discarding configuration."""
2520
+
2521
+ def construct_mapping(
2522
+ self, node: yaml.nodes.MappingNode, deep: bool = False
2523
+ ) -> dict[Any, Any]:
2524
+ keys = [
2525
+ self.construct_object(key, deep=deep)
2526
+ for key, _ in node.value
2527
+ if key.tag != "tag:yaml.org,2002:merge"
2528
+ ]
2529
+ if len(set(keys)) != len(keys):
2530
+ raise ValueError("invalid_yaml:duplicate_key")
2531
+ return super().construct_mapping(node, deep=deep)
2532
+
2533
+
2534
+ def load_yaml_client_config(content: str) -> dict[str, Any]:
2535
+ """Read a LibreChat mapping without exposing source values in parse errors."""
2536
+ try:
2537
+ payload = (
2538
+ yaml.load(content, Loader=ClientConfigSafeLoader) if content.strip() else {}
2539
+ )
2540
+ except (yaml.YAMLError, TypeError) as exc:
2541
+ raise ValueError("invalid_yaml:parse_failed") from exc
2542
+ if payload is None:
2543
+ payload = {}
2544
+ if not isinstance(payload, dict):
2545
+ raise ValueError("invalid_yaml:root_not_object")
2546
+ servers = payload.get("mcpServers", {})
2547
+ if not isinstance(servers, dict) or any(
2548
+ not isinstance(alias, str) or not isinstance(entry, dict)
2549
+ for alias, entry in servers.items()
2550
+ ):
2551
+ raise ValueError("invalid_yaml:mcpServers_not_object")
2552
+ return payload
2553
+
2554
+
2555
+ def patch_local_proxy_yaml_config(
2556
+ existing: str, generated: str, remove_unmanaged: bool
2557
+ ) -> str:
2558
+ """Merge one LibreChat connection while respecting the unmanaged-server policy."""
2559
+ current = load_yaml_client_config(existing)
2560
+ incoming = load_yaml_client_config(generated)
2561
+ entries = current.get("mcpServers", {})
2562
+ if remove_unmanaged:
2563
+ unmanaged = set(direct_server_aliases(existing, "yaml"))
2564
+ entries = {
2565
+ alias: entry for alias, entry in entries.items() if alias not in unmanaged
2566
+ }
2567
+ entries.update(incoming["mcpServers"])
2568
+ current["mcpServers"] = entries
2569
+ return yaml.safe_dump(current, sort_keys=False, allow_unicode=True)
2570
+
2571
+
2442
2572
  def patch_json_config(
2443
2573
  existing: str,
2444
2574
  generated: str,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lightnow-cli"
7
- version = "1.6.1"
7
+ version = "1.7.1"
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"}