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.
- lightnow_cli-1.7.0/.release-please-manifest.json +3 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/CHANGELOG.md +7 -0
- lightnow_cli-1.7.0/PKG-INFO +138 -0
- lightnow_cli-1.7.0/README.md +90 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/__init__.py +1 -1
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/integrations.py +111 -10
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/pyproject.toml +1 -1
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_integrations.py +305 -1
- lightnow_cli-1.6.1/.release-please-manifest.json +0 -3
- lightnow_cli-1.6.1/PKG-INFO +0 -344
- lightnow_cli-1.6.1/README.md +0 -296
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.github/workflows/ci.yml +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.github/workflows/release-please.yml +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.github/workflows/release.yml +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/.gitignore +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/CONTRIBUTING.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/LICENSE +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/Makefile +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/SECURITY.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/ci/create_client_artifact.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/ci/test_client_artifact.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/docs/development.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/docs/release.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/README.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/docs.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/openapi.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/examples/server.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/authenticated_http.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/client.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/__init__.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/auth.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/context.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/publish.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/query.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/runner.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/updates.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/commands/validate.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/config.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/main.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/target.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/tls.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/updates.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/lightnow_cli/validation.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/release-please-config.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/renovate.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/__init__.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_auth_flow.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_authenticated_http.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_cli.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_client.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_config.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_connection_target.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_context.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_tls.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_updates.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.0}/tests/test_validation.py +0 -0
|
@@ -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
|
+
[](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
|
+
[](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).
|
|
@@ -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,
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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"
|
|
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.
|
|
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"}
|