lightnow-cli 1.6.1__tar.gz → 1.7.0__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 (56) hide show
  1. lightnow_cli-1.7.0/.release-please-manifest.json +3 -0
  2. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/CHANGELOG.md +7 -0
  3. lightnow_cli-1.7.0/PKG-INFO +138 -0
  4. lightnow_cli-1.7.0/README.md +90 -0
  5. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/__init__.py +1 -1
  6. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/integrations.py +111 -10
  7. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/pyproject.toml +1 -1
  8. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_integrations.py +305 -1
  9. lightnow_cli-1.6.1/.release-please-manifest.json +0 -3
  10. lightnow_cli-1.6.1/PKG-INFO +0 -344
  11. lightnow_cli-1.6.1/README.md +0 -296
  12. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.github/workflows/ci.yml +0 -0
  13. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.github/workflows/release-please.yml +0 -0
  14. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.github/workflows/release.yml +0 -0
  15. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.gitignore +0 -0
  16. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/CONTRIBUTING.md +0 -0
  17. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/LICENSE +0 -0
  18. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/Makefile +0 -0
  19. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/SECURITY.md +0 -0
  20. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/ci/create_client_artifact.py +0 -0
  21. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/ci/test_client_artifact.py +0 -0
  22. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/docs/development.md +0 -0
  23. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/docs/release.md +0 -0
  24. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/README.md +0 -0
  25. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/docs.md +0 -0
  26. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/openapi.json +0 -0
  27. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/server.json +0 -0
  28. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/authenticated_http.py +0 -0
  29. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/client.py +0 -0
  30. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/__init__.py +0 -0
  31. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/auth.py +0 -0
  32. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/context.py +0 -0
  33. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/publish.py +0 -0
  34. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/query.py +0 -0
  35. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/runner.py +0 -0
  36. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/updates.py +0 -0
  37. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/validate.py +0 -0
  38. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/config.py +0 -0
  39. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/main.py +0 -0
  40. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/target.py +0 -0
  41. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/tls.py +0 -0
  42. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/updates.py +0 -0
  43. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/validation.py +0 -0
  44. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/release-please-config.json +0 -0
  45. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/renovate.json +0 -0
  46. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/__init__.py +0 -0
  47. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_auth_flow.py +0 -0
  48. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_authenticated_http.py +0 -0
  49. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_cli.py +0 -0
  50. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_client.py +0 -0
  51. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_config.py +0 -0
  52. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_connection_target.py +0 -0
  53. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_context.py +0 -0
  54. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_tls.py +0 -0
  55. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_updates.py +0 -0
  56. {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_validation.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.7.0"
3
+ }
@@ -4,6 +4,13 @@ All notable changes to the LightNow CLI are documented here.
4
4
 
5
5
  This project follows semantic versioning.
6
6
 
7
+ ## [1.7.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.1...v1.7.0) (2026-11-13)
8
+
9
+
10
+ ### Features
11
+
12
+ * 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))
13
+
7
14
  ## [1.6.1](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.0...v1.6.1) (2026-11-09)
8
15
 
9
16
 
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: lightnow-cli
3
+ Version: 1.7.0
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.0" # 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
@@ -965,6 +989,12 @@ def build_local_proxy_export(
965
989
  if support_error:
966
990
  raise ValueError(support_error)
967
991
  if local_proxy_transport == "stdio":
992
+ if client == "librechat" and export_format == "yaml":
993
+ return render_local_proxy_librechat_yaml(
994
+ local_proxy_config_path
995
+ or default_local_proxy_config_path(client, connection_alias),
996
+ connection_alias,
997
+ )
968
998
  if client in LOCAL_PROXY_MCP_SERVERS_JSON_CLIENTS and export_format == "json":
969
999
  return render_local_proxy_mcp_servers_json(
970
1000
  local_proxy_config_path
@@ -1099,6 +1129,18 @@ def render_local_proxy_mcp_servers_json(
1099
1129
  return json.dumps(payload, indent=2) + "\n"
1100
1130
 
1101
1131
 
1132
+ def render_local_proxy_librechat_yaml(
1133
+ local_proxy_config_path: Path, connection_alias: str = "lightnow"
1134
+ ) -> str:
1135
+ """Render an operator-owned LibreChat stdio entry on its backend host."""
1136
+ payload = json.loads(
1137
+ render_local_proxy_mcp_servers_json(local_proxy_config_path, connection_alias)
1138
+ )
1139
+ for entry in payload["mcpServers"].values():
1140
+ entry["type"] = "stdio"
1141
+ return yaml.safe_dump(payload, sort_keys=False, allow_unicode=True)
1142
+
1143
+
1102
1144
  def render_local_proxy_vscode_json(
1103
1145
  local_proxy_config_path: Path, connection_alias: str = "lightnow"
1104
1146
  ) -> str:
@@ -2084,6 +2126,11 @@ def extract_mcp_entries(content: str, export_format: str) -> dict[str, dict[str,
2084
2126
  if isinstance(entry, dict):
2085
2127
  entries[str(alias)] = entry
2086
2128
  return entries
2129
+ if export_format == "yaml":
2130
+ payload = load_yaml_client_config(content)
2131
+ return {
2132
+ str(alias): entry for alias, entry in payload.get("mcpServers", {}).items()
2133
+ }
2087
2134
  raise ValueError(f"unsupported_format:{export_format}")
2088
2135
 
2089
2136
 
@@ -2106,7 +2153,7 @@ def is_local_proxy_url(value: str) -> bool:
2106
2153
  def warning_hint(code: str, client: str) -> Optional[str]:
2107
2154
  """Return a short recovery hint for a posture warning code."""
2108
2155
  sync_command = f"lightnow sync --client {client} --local-proxy"
2109
- if code.startswith("invalid_json") or code.startswith("invalid_toml"):
2156
+ if code.startswith(("invalid_json", "invalid_toml", "invalid_yaml")):
2110
2157
  return (
2111
2158
  "the config file could not be parsed; fix it or restore the "
2112
2159
  ".lightnow.bak backup, then re-run the sync."
@@ -2439,6 +2486,60 @@ def merge_local_proxy_toml_connection(
2439
2486
  return "\n\n".join(part for part in (kept, generated.strip()) if part)
2440
2487
 
2441
2488
 
2489
+ class ClientConfigSafeLoader(yaml.SafeLoader):
2490
+ """Reject duplicate YAML keys rather than silently discarding configuration."""
2491
+
2492
+ def construct_mapping(
2493
+ self, node: yaml.nodes.MappingNode, deep: bool = False
2494
+ ) -> dict[Any, Any]:
2495
+ keys = [
2496
+ self.construct_object(key, deep=deep)
2497
+ for key, _ in node.value
2498
+ if key.tag != "tag:yaml.org,2002:merge"
2499
+ ]
2500
+ if len(set(keys)) != len(keys):
2501
+ raise ValueError("invalid_yaml:duplicate_key")
2502
+ return super().construct_mapping(node, deep=deep)
2503
+
2504
+
2505
+ def load_yaml_client_config(content: str) -> dict[str, Any]:
2506
+ """Read a LibreChat mapping without exposing source values in parse errors."""
2507
+ try:
2508
+ payload = (
2509
+ yaml.load(content, Loader=ClientConfigSafeLoader) if content.strip() else {}
2510
+ )
2511
+ except (yaml.YAMLError, TypeError) as exc:
2512
+ raise ValueError("invalid_yaml:parse_failed") from exc
2513
+ if payload is None:
2514
+ payload = {}
2515
+ if not isinstance(payload, dict):
2516
+ raise ValueError("invalid_yaml:root_not_object")
2517
+ servers = payload.get("mcpServers", {})
2518
+ if not isinstance(servers, dict) or any(
2519
+ not isinstance(alias, str) or not isinstance(entry, dict)
2520
+ for alias, entry in servers.items()
2521
+ ):
2522
+ raise ValueError("invalid_yaml:mcpServers_not_object")
2523
+ return payload
2524
+
2525
+
2526
+ def patch_local_proxy_yaml_config(
2527
+ existing: str, generated: str, remove_unmanaged: bool
2528
+ ) -> str:
2529
+ """Merge one LibreChat connection while respecting the unmanaged-server policy."""
2530
+ current = load_yaml_client_config(existing)
2531
+ incoming = load_yaml_client_config(generated)
2532
+ entries = current.get("mcpServers", {})
2533
+ if remove_unmanaged:
2534
+ unmanaged = set(direct_server_aliases(existing, "yaml"))
2535
+ entries = {
2536
+ alias: entry for alias, entry in entries.items() if alias not in unmanaged
2537
+ }
2538
+ entries.update(incoming["mcpServers"])
2539
+ current["mcpServers"] = entries
2540
+ return yaml.safe_dump(current, sort_keys=False, allow_unicode=True)
2541
+
2542
+
2442
2543
  def patch_json_config(
2443
2544
  existing: str,
2444
2545
  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.0"
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"}