nds-mapviewer 2026.4.4.dev270__tar.gz → 2026.4.5.dev294__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.
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/CHANGELOG.md +4 -0
- {nds_mapviewer-2026.4.4.dev270/src/nds_mapviewer.egg-info → nds_mapviewer-2026.4.5.dev294}/PKG-INFO +53 -4
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/README.md +52 -3
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294/src/nds_mapviewer.egg-info}/PKG-INFO +53 -4
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/browser.py +13 -4
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/cli.py +42 -5
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/clipboard.py +3 -3
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/config.py +29 -4
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/container_runtime.py +12 -6
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/docker_client.py +51 -15
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/exceptions.py +1 -1
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/tui.py +93 -4
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/viewer.py +82 -11
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/tests/test_cli.py +84 -15
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/tests/test_docker_client.py +9 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/tests/test_tui.py +27 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/tests/test_viewer.py +75 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/.gitignore +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/pyproject.toml +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/setup.cfg +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/setup.py +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/nds_mapviewer.egg-info/SOURCES.txt +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/nds_mapviewer.egg-info/dependency_links.txt +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/nds_mapviewer.egg-info/entry_points.txt +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/nds_mapviewer.egg-info/requires.txt +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/nds_mapviewer.egg-info/scm_file_list.json +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/nds_mapviewer.egg-info/top_level.txt +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/__init__.py +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/ui.py +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/tests/conftest.py +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/tests/test_clipboard.py +0 -0
- {nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/tests/test_config.py +0 -0
|
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- Enable unauthenticated MCP through the launcher with `--mcp-local`, using
|
|
13
|
+
ordinary bind/network settings and the selected host port for discovery
|
|
14
|
+
(requires a supporting container image).
|
|
15
|
+
- Set a public MCP URL with `--mcp-endpoint` for LAN access.
|
|
12
16
|
- `--debug` flag to enable container debug logging (sets `HTTP_LOG_LEVEL=debug`)
|
|
13
17
|
- `--env KEY=VALUE` (repeatable, short `-E`) to pass custom environment
|
|
14
18
|
variables into the container; overrides `--debug` on key collision
|
{nds_mapviewer-2026.4.4.dev270/src/nds_mapviewer.egg-info → nds_mapviewer-2026.4.5.dev294}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nds-mapviewer
|
|
3
|
-
Version: 2026.4.
|
|
3
|
+
Version: 2026.4.5.dev294
|
|
4
4
|
Summary: CLI to run the NDS MapViewer Docker container for visualizing map data (NDS.Live, GeoJSON, and more)
|
|
5
5
|
Author-email: NDS Association <support@nds-association.org>
|
|
6
6
|
License: Proprietary
|
|
@@ -121,9 +121,55 @@ appear in the Docker screen's "Copy Command" output.
|
|
|
121
121
|
|
|
122
122
|
`--network NAME` attaches the container to a specific network (e.g. a custom
|
|
123
123
|
bridge or `host`). With `--network host` the container binds the host's port
|
|
124
|
-
directly, so `--port
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
directly, so `--port` is ignored and the MapViewer is reached on its internal
|
|
125
|
+
port (8089). With `--mcp-local`, `--bind` selects the native listener in this mode;
|
|
126
|
+
otherwise it is ignored. Host networking behaves differently across platforms
|
|
127
|
+
and runtimes — it is intended as a connectivity workaround.
|
|
128
|
+
|
|
129
|
+
### Local MCP
|
|
130
|
+
|
|
131
|
+
MCP requires **Member Edition**. Select `--edition member` and use `--mcp-local`
|
|
132
|
+
with a supporting Member image. Community Edition does not provide MCP tools or
|
|
133
|
+
browser-agent access. No peer addresses are needed; ordinary server networking applies:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mapviewer --edition member --demo --mcp-local --port 8099
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Connect your MCP client to `http://localhost:8099/mcp` and open the viewer at
|
|
140
|
+
`http://localhost:8099` for browser-control tools. Without `--port`, the endpoint
|
|
141
|
+
uses the free port selected by the launcher, shown in the run screen. The Docker
|
|
142
|
+
screen shows the generated native options in its copyable command.
|
|
143
|
+
|
|
144
|
+
For LAN access, set the public URL explicitly so native MCP discovery and its
|
|
145
|
+
default Host/Origin allowlists use the address seen by remote browsers. For
|
|
146
|
+
example, replace `192.168.1.50` with your server's LAN address:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
mapviewer --edition member --demo --mcp-local --bind 0.0.0.0 --port 8099 --no-browser \
|
|
150
|
+
--mcp-endpoint http://192.168.1.50:8099/mcp
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Open `http://192.168.1.50:8099` and connect the MCP client to that URL plus `/mcp`.
|
|
154
|
+
The override describes the public URL; it does not change the listener, enable
|
|
155
|
+
TLS or change the launcher's ordinary localhost browser URL/readiness probe.
|
|
156
|
+
`--mcp-endpoint` requires `--mcp-local`; the native server validates URL syntax.
|
|
157
|
+
|
|
158
|
+
**Local MCP is unauthenticated. Everyone who can reach the server shares access
|
|
159
|
+
to its enabled MCP tools.** The name `local` does not enforce loopback access.
|
|
160
|
+
The ordinary `--bind` and `--network` settings control reachability: the default
|
|
161
|
+
bind is `127.0.0.1`; `--bind 0.0.0.0` or the TUI's **Allow network access** checkbox
|
|
162
|
+
exposes the published port on all host interfaces. Use firewall, container or
|
|
163
|
+
proxy policy to limit access, or OAuth when authentication is required. See the
|
|
164
|
+
[container setup](../docs/mv-mcp-workflows.md) and
|
|
165
|
+
[native MCP documentation](../deps/mapget/docs/mapget-mcp.md).
|
|
166
|
+
|
|
167
|
+
On hosts supporting host networking, `mapviewer --edition member --demo --network host --mcp-local`
|
|
168
|
+
instead uses port 8089 directly and honors `--bind` for the native listener.
|
|
169
|
+
The Python API accepts `edition="member"`, `mcp_local=True` and optional
|
|
170
|
+
`mcp_endpoint="http://host:port/mcp"` arguments to `MapViewer`.
|
|
171
|
+
|
|
172
|
+
### Local configuration
|
|
127
173
|
|
|
128
174
|
If `~/.nds/config.yaml` exists, `mapviewer` (with no arguments) loads it
|
|
129
175
|
automatically and goes straight to the RunScreen.
|
|
@@ -150,6 +196,9 @@ durable map-preset write target.
|
|
|
150
196
|
| **community** | NDS.Live, GeoJSON, open formats | Free — [nds.live](https://nds.live) |
|
|
151
197
|
| **member** | + NDS.Classic | [NDS membership](https://nds-association.org/#contact) |
|
|
152
198
|
|
|
199
|
+
MCP server access, browser-agent tools and MCP tunnels are available only in
|
|
200
|
+
Member Edition. Selecting Community with `--mcp-local` is rejected before launch.
|
|
201
|
+
|
|
153
202
|
## Open-Source Building Blocks
|
|
154
203
|
|
|
155
204
|
- **[mapget](https://github.com/ndsev/mapget)** — tile-based map data server with caching
|
|
@@ -88,9 +88,55 @@ appear in the Docker screen's "Copy Command" output.
|
|
|
88
88
|
|
|
89
89
|
`--network NAME` attaches the container to a specific network (e.g. a custom
|
|
90
90
|
bridge or `host`). With `--network host` the container binds the host's port
|
|
91
|
-
directly, so `--port
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
directly, so `--port` is ignored and the MapViewer is reached on its internal
|
|
92
|
+
port (8089). With `--mcp-local`, `--bind` selects the native listener in this mode;
|
|
93
|
+
otherwise it is ignored. Host networking behaves differently across platforms
|
|
94
|
+
and runtimes — it is intended as a connectivity workaround.
|
|
95
|
+
|
|
96
|
+
### Local MCP
|
|
97
|
+
|
|
98
|
+
MCP requires **Member Edition**. Select `--edition member` and use `--mcp-local`
|
|
99
|
+
with a supporting Member image. Community Edition does not provide MCP tools or
|
|
100
|
+
browser-agent access. No peer addresses are needed; ordinary server networking applies:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
mapviewer --edition member --demo --mcp-local --port 8099
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Connect your MCP client to `http://localhost:8099/mcp` and open the viewer at
|
|
107
|
+
`http://localhost:8099` for browser-control tools. Without `--port`, the endpoint
|
|
108
|
+
uses the free port selected by the launcher, shown in the run screen. The Docker
|
|
109
|
+
screen shows the generated native options in its copyable command.
|
|
110
|
+
|
|
111
|
+
For LAN access, set the public URL explicitly so native MCP discovery and its
|
|
112
|
+
default Host/Origin allowlists use the address seen by remote browsers. For
|
|
113
|
+
example, replace `192.168.1.50` with your server's LAN address:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
mapviewer --edition member --demo --mcp-local --bind 0.0.0.0 --port 8099 --no-browser \
|
|
117
|
+
--mcp-endpoint http://192.168.1.50:8099/mcp
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Open `http://192.168.1.50:8099` and connect the MCP client to that URL plus `/mcp`.
|
|
121
|
+
The override describes the public URL; it does not change the listener, enable
|
|
122
|
+
TLS or change the launcher's ordinary localhost browser URL/readiness probe.
|
|
123
|
+
`--mcp-endpoint` requires `--mcp-local`; the native server validates URL syntax.
|
|
124
|
+
|
|
125
|
+
**Local MCP is unauthenticated. Everyone who can reach the server shares access
|
|
126
|
+
to its enabled MCP tools.** The name `local` does not enforce loopback access.
|
|
127
|
+
The ordinary `--bind` and `--network` settings control reachability: the default
|
|
128
|
+
bind is `127.0.0.1`; `--bind 0.0.0.0` or the TUI's **Allow network access** checkbox
|
|
129
|
+
exposes the published port on all host interfaces. Use firewall, container or
|
|
130
|
+
proxy policy to limit access, or OAuth when authentication is required. See the
|
|
131
|
+
[container setup](../docs/mv-mcp-workflows.md) and
|
|
132
|
+
[native MCP documentation](../deps/mapget/docs/mapget-mcp.md).
|
|
133
|
+
|
|
134
|
+
On hosts supporting host networking, `mapviewer --edition member --demo --network host --mcp-local`
|
|
135
|
+
instead uses port 8089 directly and honors `--bind` for the native listener.
|
|
136
|
+
The Python API accepts `edition="member"`, `mcp_local=True` and optional
|
|
137
|
+
`mcp_endpoint="http://host:port/mcp"` arguments to `MapViewer`.
|
|
138
|
+
|
|
139
|
+
### Local configuration
|
|
94
140
|
|
|
95
141
|
If `~/.nds/config.yaml` exists, `mapviewer` (with no arguments) loads it
|
|
96
142
|
automatically and goes straight to the RunScreen.
|
|
@@ -117,6 +163,9 @@ durable map-preset write target.
|
|
|
117
163
|
| **community** | NDS.Live, GeoJSON, open formats | Free — [nds.live](https://nds.live) |
|
|
118
164
|
| **member** | + NDS.Classic | [NDS membership](https://nds-association.org/#contact) |
|
|
119
165
|
|
|
166
|
+
MCP server access, browser-agent tools and MCP tunnels are available only in
|
|
167
|
+
Member Edition. Selecting Community with `--mcp-local` is rejected before launch.
|
|
168
|
+
|
|
120
169
|
## Open-Source Building Blocks
|
|
121
170
|
|
|
122
171
|
- **[mapget](https://github.com/ndsev/mapget)** — tile-based map data server with caching
|
{nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294/src/nds_mapviewer.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nds-mapviewer
|
|
3
|
-
Version: 2026.4.
|
|
3
|
+
Version: 2026.4.5.dev294
|
|
4
4
|
Summary: CLI to run the NDS MapViewer Docker container for visualizing map data (NDS.Live, GeoJSON, and more)
|
|
5
5
|
Author-email: NDS Association <support@nds-association.org>
|
|
6
6
|
License: Proprietary
|
|
@@ -121,9 +121,55 @@ appear in the Docker screen's "Copy Command" output.
|
|
|
121
121
|
|
|
122
122
|
`--network NAME` attaches the container to a specific network (e.g. a custom
|
|
123
123
|
bridge or `host`). With `--network host` the container binds the host's port
|
|
124
|
-
directly, so `--port
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
directly, so `--port` is ignored and the MapViewer is reached on its internal
|
|
125
|
+
port (8089). With `--mcp-local`, `--bind` selects the native listener in this mode;
|
|
126
|
+
otherwise it is ignored. Host networking behaves differently across platforms
|
|
127
|
+
and runtimes — it is intended as a connectivity workaround.
|
|
128
|
+
|
|
129
|
+
### Local MCP
|
|
130
|
+
|
|
131
|
+
MCP requires **Member Edition**. Select `--edition member` and use `--mcp-local`
|
|
132
|
+
with a supporting Member image. Community Edition does not provide MCP tools or
|
|
133
|
+
browser-agent access. No peer addresses are needed; ordinary server networking applies:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mapviewer --edition member --demo --mcp-local --port 8099
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Connect your MCP client to `http://localhost:8099/mcp` and open the viewer at
|
|
140
|
+
`http://localhost:8099` for browser-control tools. Without `--port`, the endpoint
|
|
141
|
+
uses the free port selected by the launcher, shown in the run screen. The Docker
|
|
142
|
+
screen shows the generated native options in its copyable command.
|
|
143
|
+
|
|
144
|
+
For LAN access, set the public URL explicitly so native MCP discovery and its
|
|
145
|
+
default Host/Origin allowlists use the address seen by remote browsers. For
|
|
146
|
+
example, replace `192.168.1.50` with your server's LAN address:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
mapviewer --edition member --demo --mcp-local --bind 0.0.0.0 --port 8099 --no-browser \
|
|
150
|
+
--mcp-endpoint http://192.168.1.50:8099/mcp
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Open `http://192.168.1.50:8099` and connect the MCP client to that URL plus `/mcp`.
|
|
154
|
+
The override describes the public URL; it does not change the listener, enable
|
|
155
|
+
TLS or change the launcher's ordinary localhost browser URL/readiness probe.
|
|
156
|
+
`--mcp-endpoint` requires `--mcp-local`; the native server validates URL syntax.
|
|
157
|
+
|
|
158
|
+
**Local MCP is unauthenticated. Everyone who can reach the server shares access
|
|
159
|
+
to its enabled MCP tools.** The name `local` does not enforce loopback access.
|
|
160
|
+
The ordinary `--bind` and `--network` settings control reachability: the default
|
|
161
|
+
bind is `127.0.0.1`; `--bind 0.0.0.0` or the TUI's **Allow network access** checkbox
|
|
162
|
+
exposes the published port on all host interfaces. Use firewall, container or
|
|
163
|
+
proxy policy to limit access, or OAuth when authentication is required. See the
|
|
164
|
+
[container setup](../docs/mv-mcp-workflows.md) and
|
|
165
|
+
[native MCP documentation](../deps/mapget/docs/mapget-mcp.md).
|
|
166
|
+
|
|
167
|
+
On hosts supporting host networking, `mapviewer --edition member --demo --network host --mcp-local`
|
|
168
|
+
instead uses port 8089 directly and honors `--bind` for the native listener.
|
|
169
|
+
The Python API accepts `edition="member"`, `mcp_local=True` and optional
|
|
170
|
+
`mcp_endpoint="http://host:port/mcp"` arguments to `MapViewer`.
|
|
171
|
+
|
|
172
|
+
### Local configuration
|
|
127
173
|
|
|
128
174
|
If `~/.nds/config.yaml` exists, `mapviewer` (with no arguments) loads it
|
|
129
175
|
automatically and goes straight to the RunScreen.
|
|
@@ -150,6 +196,9 @@ durable map-preset write target.
|
|
|
150
196
|
| **community** | NDS.Live, GeoJSON, open formats | Free — [nds.live](https://nds.live) |
|
|
151
197
|
| **member** | + NDS.Classic | [NDS membership](https://nds-association.org/#contact) |
|
|
152
198
|
|
|
199
|
+
MCP server access, browser-agent tools and MCP tunnels are available only in
|
|
200
|
+
Member Edition. Selecting Community with `--mcp-local` is rejected before launch.
|
|
201
|
+
|
|
153
202
|
## Open-Source Building Blocks
|
|
154
203
|
|
|
155
204
|
- **[mapget](https://github.com/ndsev/mapget)** — tile-based map data server with caching
|
{nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/browser.py
RENAMED
|
@@ -97,7 +97,10 @@ def is_playwright_available() -> bool:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def is_playwright_chromium_installed() -> bool:
|
|
100
|
-
"""Check
|
|
100
|
+
"""Check whether Playwright exposes a Chromium executable path.
|
|
101
|
+
|
|
102
|
+
This probe does not check that the file exists or that Chromium can launch.
|
|
103
|
+
"""
|
|
101
104
|
if not is_playwright_available():
|
|
102
105
|
return False
|
|
103
106
|
|
|
@@ -135,13 +138,19 @@ def open_browser(
|
|
|
135
138
|
"""
|
|
136
139
|
Open a URL in a web browser.
|
|
137
140
|
|
|
141
|
+
Bundled Chromium blocks until the user presses Enter and is then closed.
|
|
142
|
+
System-browser launches return immediately; they do not verify that the
|
|
143
|
+
page loaded. Missing Playwright or a failed browser-path probe raises before
|
|
144
|
+
launch, while errors during launch/navigation return False.
|
|
145
|
+
|
|
138
146
|
Args:
|
|
139
147
|
url: The URL to open
|
|
140
148
|
use_bundled: Use playwright's bundled Chromium
|
|
141
149
|
prefer_chromium: Prefer Chromium-based browsers for best performance
|
|
142
150
|
|
|
143
151
|
Returns:
|
|
144
|
-
True if the
|
|
152
|
+
True if the launch path completed without an exception. The default
|
|
153
|
+
browser's own boolean result is not checked.
|
|
145
154
|
"""
|
|
146
155
|
# Option 1: Use bundled Chromium via playwright
|
|
147
156
|
if use_bundled:
|
|
@@ -152,7 +161,7 @@ def open_browser(
|
|
|
152
161
|
)
|
|
153
162
|
|
|
154
163
|
if not is_playwright_chromium_installed():
|
|
155
|
-
#
|
|
164
|
+
# Installation remains an explicit caller/user action.
|
|
156
165
|
raise RuntimeError(
|
|
157
166
|
"Playwright Chromium is not installed. Install with:\n"
|
|
158
167
|
" playwright install chromium"
|
|
@@ -164,7 +173,7 @@ def open_browser(
|
|
|
164
173
|
browser = p.chromium.launch(headless=False)
|
|
165
174
|
page = browser.new_page()
|
|
166
175
|
page.goto(url)
|
|
167
|
-
|
|
176
|
+
# Keep the browser open until the user presses Enter.
|
|
168
177
|
input("Press Enter to close the browser...")
|
|
169
178
|
browser.close()
|
|
170
179
|
return True
|
{nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/cli.py
RENAMED
|
@@ -28,7 +28,7 @@ def _build_run_subtitle(edition: str, image_version_raw: str | None) -> str:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _stop_existing_container() -> None:
|
|
31
|
-
"""Stop the
|
|
31
|
+
"""Stop and remove the default MapViewer container if it exists."""
|
|
32
32
|
from .viewer import MapViewer
|
|
33
33
|
from .exceptions import ContainerNotFound
|
|
34
34
|
|
|
@@ -40,7 +40,7 @@ def _stop_existing_container() -> None:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def _get_edition_default() -> str:
|
|
43
|
-
"""
|
|
43
|
+
"""Use the edition saved by ndslive-setup, then local member credentials, else community."""
|
|
44
44
|
setup_config = Path.home() / ".nds" / "setup.json"
|
|
45
45
|
if setup_config.exists():
|
|
46
46
|
import json
|
|
@@ -109,7 +109,14 @@ def _build_environment(args: argparse.Namespace) -> dict[str, str]:
|
|
|
109
109
|
# ---------------------------------------------------------------------------
|
|
110
110
|
|
|
111
111
|
def cmd_main(args: argparse.Namespace) -> int:
|
|
112
|
-
"""
|
|
112
|
+
"""Resolve launch inputs and run the corresponding TUI until it exits.
|
|
113
|
+
|
|
114
|
+
Demo mode takes precedence. Without explicit sources, try the default then
|
|
115
|
+
legacy config before showing HomeScreen. Explicit inputs combine their
|
|
116
|
+
sources and HTTP settings; retain a config path for live reload only when
|
|
117
|
+
the sole input is a YAML config. Return 1 for source resolution errors and
|
|
118
|
+
2 for incompatible MCP flags.
|
|
119
|
+
"""
|
|
113
120
|
from .config import (
|
|
114
121
|
load_config,
|
|
115
122
|
resolve_config_source,
|
|
@@ -133,6 +140,14 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
133
140
|
source_args = args.sources
|
|
134
141
|
environment = _build_environment(args)
|
|
135
142
|
network = getattr(args, "network", None)
|
|
143
|
+
mcp_local = args.mcp_local
|
|
144
|
+
mcp_endpoint = args.mcp_endpoint
|
|
145
|
+
if mcp_endpoint is not None and not mcp_local:
|
|
146
|
+
console.print("[red]Error:[/red] --mcp-endpoint requires --mcp-local.")
|
|
147
|
+
return 2
|
|
148
|
+
if mcp_local and edition != "member":
|
|
149
|
+
console.print("[red]Error:[/red] MCP requires MapViewer Member Edition. Select --edition member.")
|
|
150
|
+
return 2
|
|
136
151
|
|
|
137
152
|
# --demo → demo config
|
|
138
153
|
if args.demo:
|
|
@@ -146,6 +161,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
146
161
|
pull_policy=pull_policy,
|
|
147
162
|
environment=environment,
|
|
148
163
|
network=network,
|
|
164
|
+
mcp_local=mcp_local,
|
|
165
|
+
mcp_endpoint=mcp_endpoint,
|
|
149
166
|
run_config=config,
|
|
150
167
|
run_label="Demo (NDS Islands)",
|
|
151
168
|
is_demo=True,
|
|
@@ -168,6 +185,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
168
185
|
pull_policy=pull_policy,
|
|
169
186
|
environment=environment,
|
|
170
187
|
network=network,
|
|
188
|
+
mcp_local=mcp_local,
|
|
189
|
+
mcp_endpoint=mcp_endpoint,
|
|
171
190
|
run_config=config,
|
|
172
191
|
run_config_path=DEFAULT_CONFIG,
|
|
173
192
|
run_label="~/.nds/config.yaml",
|
|
@@ -193,6 +212,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
193
212
|
pull_policy=pull_policy,
|
|
194
213
|
environment=environment,
|
|
195
214
|
network=network,
|
|
215
|
+
mcp_local=mcp_local,
|
|
216
|
+
mcp_endpoint=mcp_endpoint,
|
|
196
217
|
run_config=config,
|
|
197
218
|
run_config_path=LEGACY_CONFIG,
|
|
198
219
|
run_label="~/.nds/mapviewer.yaml",
|
|
@@ -211,6 +232,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
211
232
|
pull_policy=pull_policy,
|
|
212
233
|
environment=environment,
|
|
213
234
|
network=network,
|
|
235
|
+
mcp_local=mcp_local,
|
|
236
|
+
mcp_endpoint=mcp_endpoint,
|
|
214
237
|
)
|
|
215
238
|
app.run()
|
|
216
239
|
return 0
|
|
@@ -306,6 +329,8 @@ def cmd_main(args: argparse.Namespace) -> int:
|
|
|
306
329
|
pull_policy=pull_policy,
|
|
307
330
|
environment=environment,
|
|
308
331
|
network=network,
|
|
332
|
+
mcp_local=mcp_local,
|
|
333
|
+
mcp_endpoint=mcp_endpoint,
|
|
309
334
|
run_config=merged,
|
|
310
335
|
run_config_path=source_config_path,
|
|
311
336
|
run_label=source_desc,
|
|
@@ -411,8 +436,20 @@ Examples:
|
|
|
411
436
|
"--network",
|
|
412
437
|
metavar="NAME",
|
|
413
438
|
default=None,
|
|
414
|
-
help="Container network to attach (e.g. 'host'). With 'host'
|
|
415
|
-
"
|
|
439
|
+
help="Container network to attach (e.g. 'host'). With 'host', --port is "
|
|
440
|
+
"ignored (8089 is used); with --mcp-local, --bind selects the native listener.",
|
|
441
|
+
)
|
|
442
|
+
parser.add_argument(
|
|
443
|
+
"--mcp-local",
|
|
444
|
+
action="store_true",
|
|
445
|
+
help="Enable Member Edition's unauthenticated MCP with one shared user. Anyone who can "
|
|
446
|
+
"reach the server can use its enabled MCP tools; normal --bind/--network settings apply.",
|
|
447
|
+
)
|
|
448
|
+
parser.add_argument(
|
|
449
|
+
"--mcp-endpoint",
|
|
450
|
+
metavar="URL",
|
|
451
|
+
help="Public MCP URL ending in /mcp (requires --mcp-local). Defaults to the "
|
|
452
|
+
"local launcher URL; set it for LAN/public browser access. Does not change the bind address.",
|
|
416
453
|
)
|
|
417
454
|
|
|
418
455
|
pull_group = parser.add_mutually_exclusive_group()
|
{nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/clipboard.py
RENAMED
|
@@ -4,7 +4,7 @@ Textual's ``App.copy_to_clipboard`` relies on the OSC 52 terminal escape
|
|
|
4
4
|
sequence, which many Windows terminals (conhost, some Windows Terminal
|
|
5
5
|
configs, PowerShell hosts) silently drop — so the copy buttons appear to do
|
|
6
6
|
nothing. This module copies via the native OS clipboard utility first and
|
|
7
|
-
|
|
7
|
+
falls back to OSC 52 when native copying is unavailable or fails.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -65,8 +65,8 @@ def copy_text(text: str, app=None) -> bool:
|
|
|
65
65
|
"""Copy *text* to the clipboard, returning True on confirmed success.
|
|
66
66
|
|
|
67
67
|
Tries the native OS clipboard utility first (reliable on Windows where
|
|
68
|
-
OSC 52 is commonly unsupported).
|
|
69
|
-
|
|
68
|
+
OSC 52 is commonly unsupported). If native copying fails and an *app* is
|
|
69
|
+
given, falls back to Textual's OSC 52 escape — which cannot be
|
|
70
70
|
confirmed, so this returns False to let callers warn the user.
|
|
71
71
|
"""
|
|
72
72
|
if _native_copy(text):
|
{nds_mapviewer-2026.4.4.dev270 → nds_mapviewer-2026.4.5.dev294}/src/ndslive/mapviewer/config.py
RENAMED
|
@@ -286,6 +286,15 @@ def transform_config(
|
|
|
286
286
|
- Rewrites paths to use container mount points
|
|
287
287
|
- Transforms localhost references to host.docker.internal
|
|
288
288
|
- Rewrites CLI-managed erdblick asset paths to static container paths
|
|
289
|
+
|
|
290
|
+
The input is deep-copied. ``base_dir`` resolves relative browser assets;
|
|
291
|
+
local datasource URIs must already contain absolute paths. ``existing_volumes``
|
|
292
|
+
reserves the original container paths during hot reload, including mounts no
|
|
293
|
+
longer referenced. The returned plan may require new mounts: the caller must
|
|
294
|
+
check it against the running container before publishing a reload.
|
|
295
|
+
|
|
296
|
+
``ephemeral`` marks generated configs so browser preset edits are not offered
|
|
297
|
+
as durable writes to a temporary launcher-owned file.
|
|
289
298
|
"""
|
|
290
299
|
import copy
|
|
291
300
|
|
|
@@ -306,6 +315,7 @@ def transform_config(
|
|
|
306
315
|
assigned_by_host: dict[tuple[Path, str], str] = dict(existing_by_host)
|
|
307
316
|
|
|
308
317
|
def existing_container_path(host_path: Path, root: str) -> str | None:
|
|
318
|
+
"""Reuse a host mapping only within its data/style/background mount category."""
|
|
309
319
|
container_path = assigned_by_host.get((host_path.expanduser().resolve(), root))
|
|
310
320
|
if container_path == root or (
|
|
311
321
|
container_path is not None
|
|
@@ -315,6 +325,7 @@ def transform_config(
|
|
|
315
325
|
return None
|
|
316
326
|
|
|
317
327
|
def next_container_path(root: str, preferred: str) -> str:
|
|
328
|
+
"""Reserve an unused target without displacing mounts from an earlier config."""
|
|
318
329
|
candidate = preferred
|
|
319
330
|
if candidate not in used_container_paths:
|
|
320
331
|
used_container_paths.add(candidate)
|
|
@@ -329,12 +340,14 @@ def transform_config(
|
|
|
329
340
|
index += 1
|
|
330
341
|
|
|
331
342
|
def assign_volume(host_path: Path, root: str, preferred: str) -> str:
|
|
343
|
+
"""Assign one stable destination per canonical host path and mount category."""
|
|
332
344
|
host_path = host_path.expanduser().resolve()
|
|
333
345
|
container_path = existing_container_path(host_path, root)
|
|
334
346
|
if container_path is None:
|
|
335
347
|
container_path = next_container_path(root, preferred)
|
|
336
348
|
assigned_by_host[(host_path, root)] = container_path
|
|
337
349
|
return container_path
|
|
350
|
+
|
|
338
351
|
local_paths: list[Path] = []
|
|
339
352
|
path_locations: list[tuple[str, int, str, str]] = [] # (section, index, field, original)
|
|
340
353
|
|
|
@@ -430,6 +443,7 @@ def transform_config(
|
|
|
430
443
|
asset_volume_keys: set[tuple[Path, str]] = set()
|
|
431
444
|
|
|
432
445
|
def add_asset_volume(host_path: Path, root: str, preferred: str) -> str:
|
|
446
|
+
"""Deduplicate browser assets while sharing the launcher's target allocation."""
|
|
433
447
|
resolved_host_path = host_path.expanduser().resolve()
|
|
434
448
|
container_path = assign_volume(resolved_host_path, root, preferred)
|
|
435
449
|
key = (resolved_host_path, container_path)
|
|
@@ -444,6 +458,7 @@ def transform_config(
|
|
|
444
458
|
background_asset_index = 0
|
|
445
459
|
|
|
446
460
|
def transform_additional_style_url(url: str) -> str:
|
|
461
|
+
"""Mount local style files or wildcard directories and preserve remote URLs."""
|
|
447
462
|
nonlocal style_asset_index
|
|
448
463
|
url = url.strip()
|
|
449
464
|
if (
|
|
@@ -473,6 +488,7 @@ def transform_config(
|
|
|
473
488
|
return container_path
|
|
474
489
|
|
|
475
490
|
def transform_background_template(template: str) -> str:
|
|
491
|
+
"""Mount the fixed directory prefix and keep XYZ placeholders in the URL suffix."""
|
|
476
492
|
nonlocal background_asset_index
|
|
477
493
|
template = template.strip()
|
|
478
494
|
if (
|
|
@@ -569,8 +585,13 @@ def validate_paths(
|
|
|
569
585
|
base_dir: str | Path | None = None,
|
|
570
586
|
) -> list[str]:
|
|
571
587
|
"""
|
|
572
|
-
Validate that
|
|
573
|
-
|
|
588
|
+
Validate that supported datasource paths and CLI-managed erdblick assets exist.
|
|
589
|
+
|
|
590
|
+
Source URI/folder paths use the working directory for relative paths;
|
|
591
|
+
erdblick assets use ``base_dir``. External/browser asset URLs are skipped,
|
|
592
|
+
and XYZ templates require only their static root directory to exist.
|
|
593
|
+
Return accumulated path/extension errors; this does not validate the full
|
|
594
|
+
configuration schema or the contents of asset files.
|
|
574
595
|
"""
|
|
575
596
|
errors = []
|
|
576
597
|
sources = config.get("sources", [])
|
|
@@ -831,6 +852,10 @@ def create_service_config(
|
|
|
831
852
|
) -> dict[str, Any]:
|
|
832
853
|
"""Create a config dict for connecting to a remote service.
|
|
833
854
|
|
|
855
|
+
HTTP settings apply to all paths on the URL's origin. Only the selected
|
|
856
|
+
authentication method is emitted, and incomplete credentials omit that
|
|
857
|
+
method rather than raising. Proxy settings are independent of authentication.
|
|
858
|
+
|
|
834
859
|
Args:
|
|
835
860
|
url: Service URL (e.g., https://api.example.com/openapi.json)
|
|
836
861
|
service_type: SmartLayer datasource type, see ``detect_service_type``
|
|
@@ -941,7 +966,7 @@ def resolve_config_source(arg: str) -> ResolvedConfig | None:
|
|
|
941
966
|
"""Resolve a CLI argument to a config source.
|
|
942
967
|
|
|
943
968
|
Resolution order:
|
|
944
|
-
1. If arg
|
|
969
|
+
1. If arg starts with /, ./ or ../ → resolve only as a local path
|
|
945
970
|
2. If arg exists as local file/folder → use it
|
|
946
971
|
3. If ~/.nds/configs/<arg>.yaml exists → use saved config
|
|
947
972
|
4. Return None if not found
|
|
@@ -1024,7 +1049,7 @@ def resolve_config_source(arg: str) -> ResolvedConfig | None:
|
|
|
1024
1049
|
|
|
1025
1050
|
|
|
1026
1051
|
def save_config(config: dict[str, Any], name: str) -> Path:
|
|
1027
|
-
"""
|
|
1052
|
+
"""Write a named YAML config under ``~/.nds/configs``, replacing its contents.
|
|
1028
1053
|
|
|
1029
1054
|
Args:
|
|
1030
1055
|
config: Config dict to save
|
|
@@ -71,10 +71,14 @@ def detect_runtime() -> str:
|
|
|
71
71
|
|
|
72
72
|
1. Saved ``runtime_name`` in ``~/.nds/setup.json`` (written by ``ndslive-setup``).
|
|
73
73
|
2. ``CONTAINER_RUNTIME`` environment variable (``docker`` or ``podman``).
|
|
74
|
-
3.
|
|
75
|
-
4.
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
3. A responsive installed runtime, checking Docker before Podman.
|
|
75
|
+
4. The first installed runtime when neither responds, so the caller can
|
|
76
|
+
diagnose a stopped service instead of reporting a missing installation.
|
|
77
|
+
|
|
78
|
+
Saved and environment choices must exist on ``PATH`` but are not probed here.
|
|
79
|
+
An unavailable saved choice falls through; an unavailable explicit environment
|
|
80
|
+
choice raises :class:`FileNotFoundError`, as does finding no runtime at all.
|
|
81
|
+
The result is cached for this process.
|
|
78
82
|
"""
|
|
79
83
|
saved = _saved_runtime_preference()
|
|
80
84
|
if saved and _runtime_exists(saved):
|
|
@@ -160,8 +164,10 @@ def stream(
|
|
|
160
164
|
) -> subprocess.Popen[str]:
|
|
161
165
|
"""Start a long-running runtime command for line-by-line streaming.
|
|
162
166
|
|
|
163
|
-
The caller
|
|
164
|
-
|
|
167
|
+
The caller must read ``proc.stdout`` and eventually call ``proc.terminate()``
|
|
168
|
+
and/or ``proc.wait()``. With separate stderr, also read ``proc.stderr`` to
|
|
169
|
+
avoid blocking a verbose subprocess; otherwise request combined output
|
|
170
|
+
with ``merge_stderr``.
|
|
165
171
|
|
|
166
172
|
Parameters
|
|
167
173
|
----------
|