mcp-modelmanager 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Siegfried Emil Timothy Heerwagen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,544 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-modelmanager
3
+ Version: 0.1.0
4
+ Summary: MCP server to manage your own local Ollama/vLLM model machine over a secured two-channel SSH link, with a closed operation catalog.
5
+ Author: Siegfried Emil Timothy Heerwagen
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/setheerwagen/local-ai-mcp-servers
8
+ Project-URL: Repository, https://github.com/setheerwagen/local-ai-mcp-servers
9
+ Project-URL: Issues, https://github.com/setheerwagen/local-ai-mcp-servers/issues
10
+ Keywords: mcp,ollama,vllm,llm,self-hosted,local-ai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Environment :: Console
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: mcp==1.29.0
23
+ Requires-Dist: httpx==0.28.1
24
+ Dynamic: license-file
25
+
26
+ # mcp-modelmanager
27
+
28
+ **Manage your own local model machine through a language-model tool (Claude,
29
+ ChatGPT, etc.). The models and their data stay on your hardware; the server is
30
+ designed so the machine need not be opened to the internet by this software.**
31
+
32
+ This MCP server gives an AI session controlled access to **your own**
33
+ Ollama/model machine: read state, pull and remove models, switch the inference
34
+ service to a different model, build custom Ollama variants and run LoRA training.
35
+ The models run on your hardware. The server process itself talks only to your
36
+ own computer or local network, never to a public address, on its own. What it
37
+ does knowingly reach out to a public address for is a model: calling
38
+ `fetch_model` makes the model machine connect to Hugging Face at runtime to
39
+ download it. That is a deliberate, tool-triggered outbound connection, not
40
+ something the server does behind your back or only once at setup.
41
+ `switch_service` never reaches Hugging Face itself; if the model is not yet on
42
+ the machine, it refuses and tells you to run `fetch_model` first.
43
+
44
+ **What that does and does not promise.** The inference itself stays local: your
45
+ prompts to the *managed* models and their answers do not go to any cloud model
46
+ provider. But the language model you *drive this server with* (Claude, ChatGPT,
47
+ etc.) is itself a hosted service in the usual case. Your instructions to it and
48
+ the tool results it sees pass through that provider like any other MCP call.
49
+ "Local" here means the managed machine and its data; it is not a claim that
50
+ nothing you type ever reaches a third party. Choose the driving model
51
+ accordingly, or drive it with a local one.
52
+
53
+ **Operating systems:** The plain query path (reading status and models over the
54
+ tunnel) runs cross-platform. Management and setup (service and training control)
55
+ require a POSIX system with systemd (systemd user unit, `ssh -F /dev/null`);
56
+ Windows is not supported for management.
57
+
58
+ The server ships with **no** address of its own and **no** default values for a
59
+ machine. You enter where your machine is; without those details the server
60
+ deliberately does not start (see [Required fields](#required-fields) and
61
+ [SETUP.md](SETUP.md)).
62
+
63
+ ## What the server can and cannot do
64
+
65
+ **It can:** query state (GPU memory, disk space, running service, containers),
66
+ list/pull/remove models, switch the vLLM inference service to a different model
67
+ (with an up-front estimate of the memory need), build custom Ollama variants
68
+ (fixed system prompt, parameters, later an adapter) and start/track/cancel LoRA
69
+ training.
70
+
71
+ **It cannot** and deliberately will not: run an arbitrary command on the
72
+ machine. There is **no** tool for free remote control. Every operation is a fixed
73
+ template; every parameter first passes an allowlist (see [Security
74
+ model](#security-model-fixed-operations)).
75
+
76
+ **It is not a query bridge.** For merely *asking* a model you use a separate,
77
+ read-only MCP server (e.g. a simple HTTP bridge to Ollama). This server *manages*
78
+ the machine. Two separate servers ensure that a query call never accidentally
79
+ holds management rights.
80
+
81
+ ## Your machine: three possible cases
82
+
83
+ The model machine can be attached in different ways. All three cases are
84
+ supported; you only enter a different address.
85
+
86
+ | Case | Where Ollama/vLLM runs | What you enter as `MM_VM_HOST` |
87
+ |---|---|---|
88
+ | **a) same machine** | on the same computer as this server | `127.0.0.1` |
89
+ | **b) on the local network** | on another computer in the same LAN | its LAN address, e.g. `192.168.1.10` |
90
+ | **c) over tunnel/VPN** | on a remote computer, reachable via VPN/tunnel | the address at your own/VPN end, e.g. `127.0.0.1` for a local SSH tunnel, or the VPN address |
91
+
92
+ Safeguard: the server process's own MCP/SSH/HTTP traffic goes **only** to the own
93
+ computer or the local network. Addresses outside (public IPs, foreign hostnames)
94
+ are rejected as `MM_VM_HOST`, so the server does not reach out to a public
95
+ address on its own to talk to your model machine. For case c) this means: the
96
+ tunnel/VPN ends locally or in a private address range, and you enter **that**
97
+ local/private address, not the public address of the far side. This safeguard
98
+ does not cover model downloads: `fetch_model` makes the model machine itself
99
+ reach out to Hugging Face, a public address, whenever you ask it to fetch a
100
+ model (see the caveat above); `switch_service` refuses a model that is not
101
+ already on the machine instead of fetching it. `fetch_model(source="ollama")` sends
102
+ the machine to a second, equally deliberate public address: it makes the
103
+ machine's Ollama daemon pull the model via `/api/pull`, which reaches out to
104
+ `registry.ollama.ai` for plain model names; a prefixed name such as
105
+ `hf.co/user/repo` sends it to that other registry instead.
106
+
107
+ The plain **query** (state, version, loaded models) runs over HTTP to the service
108
+ port. **Management** (pulling models, switching the service, training) runs over
109
+ a secured SSH control channel. For management you therefore need SSH access to
110
+ the machine, also in case a) (then SSH to `127.0.0.1`).
111
+
112
+ ## Installation
113
+
114
+ Requirement: Python 3.11 or newer.
115
+
116
+ **With uv (recommended):**
117
+
118
+ ```
119
+ uv venv --seed .venv
120
+ .venv/bin/pip install -r requirements.txt
121
+ ```
122
+
123
+ `--seed` ensures `pip` is present in the venv.
124
+
125
+ **With Python built-ins (fallback):**
126
+
127
+ ```
128
+ python3 -m venv .venv
129
+ .venv/bin/pip install -r requirements.txt
130
+ ```
131
+
132
+ Should `python3 -m venv` produce a venv without `pip` on your system (missing
133
+ `ensurepip`), use the uv path above.
134
+
135
+ There are only two **direct** dependencies, both pinned: the MCP SDK
136
+ (`mcp==1.29.0`) and an HTTP client (`httpx==0.28.1`). These two pins live in
137
+ `requirements.txt` and `pyproject.toml`; the full resolved dependency tree
138
+ (including the transitive packages `anyio`, `pydantic`, `starlette`,
139
+ `uvicorn`, `jsonschema`, `pyjwt`) is additionally pinned in `uv.lock`. CI
140
+ checks with `uv lock --check` that the lock still matches `pyproject.toml`,
141
+ then installs the exact locked tree for the tests, so a drifted lock fails CI.
142
+ A separate, advisory `pip-audit` job checks the locked tree for known
143
+ vulnerabilities on every push to main and pull request and again weekly; it
144
+ does not block a pull request. The SSH access uses the system's `ssh` program.
145
+
146
+ ## Required fields
147
+
148
+ The server reads its configuration **exclusively** from environment variables.
149
+ These three you **must** set, otherwise it does not start:
150
+
151
+ | Field | Meaning |
152
+ |---|---|
153
+ | `MM_VM_HOST` | Address of your model machine (`127.0.0.1`, LAN IP or tunnel/VPN address) |
154
+ | `MM_VM_USER` | SSH user name on the machine (only for management over the control channel) |
155
+ | `MM_CONTAINER_ROOT` | Root directory on your model machine under which all model, adapter and cache data live |
156
+
157
+ If one is missing, the start aborts with a clear message that says **which**
158
+ field is missing. Example:
159
+
160
+ ```
161
+ $ PYTHONPATH=src .venv/bin/python -m mcp_modelmanager.server
162
+ MM_CONTAINER_ROOT is not set. Enter the root directory on your model machine
163
+ under which all model, adapter and cache data live. Without this value the
164
+ server does not start. See SETUP.md.
165
+ ```
166
+
167
+ All further fields have sensible defaults (Ollama and vLLM standard ports, key
168
+ storage in your own `~/.ssh`) and are only set when needed; see [Environment
169
+ variables](#environment-variables).
170
+
171
+ The **full** step-by-step setup from the empty state to the running server
172
+ (including the secured SSH channels) is in [SETUP.md](SETUP.md). Building the
173
+ GPU **model machine itself** (an Ollama and/or vLLM install reachable under
174
+ the `MM_*` variables below) is the machine-side task, and it ships **with this
175
+ repo**: the operator-side scripts (`setup/`, `vm_side/`), the network fence
176
+ templates (`security/`) and the machine-side guides ([MACHINE.md](MACHINE.md),
177
+ [SECURITY.md](SECURITY.md), [SETUP-MACHINE.md](SETUP-MACHINE.md)) sit next to
178
+ the server here. Start from [MACHINE.md](MACHINE.md) for the machine side.
179
+
180
+ ## MCP registration (example with placeholders)
181
+
182
+ Register the server with your language-model tool. Replace the placeholders in
183
+ angle brackets with your values. Two ways to point the command at the server,
184
+ both supported:
185
+
186
+ **From a checkout** (no install step, `PYTHONPATH` points at `src/`):
187
+
188
+ ```json
189
+ {
190
+ "mcpServers": {
191
+ "modelmanager": {
192
+ "command": "/path/to/mcp-modelmanager/.venv/bin/python",
193
+ "args": ["-m", "mcp_modelmanager.server"],
194
+ "cwd": "/path/to/mcp-modelmanager",
195
+ "env": {
196
+ "PYTHONPATH": "src",
197
+ "MM_ACCESS": "direct",
198
+ "MM_VM_HOST": "<your-machine-or-127.0.0.1>",
199
+ "MM_VM_USER": "<your-ssh-user>",
200
+ "MM_CONTAINER_ROOT": "/srv/models"
201
+ }
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ **After `pip install .`** (the console entry point, no `cwd`/`PYTHONPATH` needed):
208
+
209
+ ```json
210
+ {
211
+ "mcpServers": {
212
+ "modelmanager": {
213
+ "command": "/path/to/mcp-modelmanager/.venv/bin/mcp-modelmanager",
214
+ "env": {
215
+ "MM_ACCESS": "direct",
216
+ "MM_VM_HOST": "<your-machine-or-127.0.0.1>",
217
+ "MM_VM_USER": "<your-ssh-user>",
218
+ "MM_CONTAINER_ROOT": "/srv/models"
219
+ }
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ Both blocks follow the plain `mcpServers` JSON shape most MCP clients read
226
+ (Claude Code, Claude Desktop and any other MCP client that reads an `mcpServers`
227
+ JSON block); consult your client's docs for where this block goes.
228
+
229
+ `MM_ACCESS` is `direct` (the server talks to the service ports under the given
230
+ address) or `tunnel` (it talks to `127.0.0.1` and builds an SSH tunnel on demand;
231
+ only sensible after the isolation, see SETUP.md). The default is `tunnel`; the
232
+ example above sets `direct` because that is what a case a)/b) setup without the
233
+ level-2 tunnel needs, so set `MM_ACCESS=direct` explicitly until that tunnel is
234
+ in place.
235
+
236
+ ## Security model: fixed operations
237
+
238
+ There is **no** tool that runs an arbitrary command. Every operation is a fixed
239
+ template in `operations.py`. The caller can only fill named placeholders, and
240
+ every value must first pass an **allowlist** (`validation.py`): permitted is what
241
+ explicitly matches the agreed form. It is **not** filtered out, it is allowed in.
242
+ On substitution it is additionally quoted with `shlex.quote`; that is the net
243
+ below the net, not the safeguard.
244
+
245
+ Checked on **both** sides: here before sending and, independently again, by a
246
+ counterpart running on the model machine, which is bound to the
247
+ control-channel key. So even if that key is stolen, only the closed set of
248
+ operations is reachable.
249
+
250
+ Also: only targets on the own computer or the local network, names are resolved
251
+ and re-checked at each use (no address pinning like the query server; prefer
252
+ setting `MM_VM_HOST` to an IP if you use a hostname). No credentials in the
253
+ code, no `.env`.
254
+ The two SSH keys live **outside** the repo in your `~/.ssh`; the `.gitignore`
255
+ additionally excludes key patterns.
256
+
257
+ How the secured two-channel architecture (data channel for the tunnel, control
258
+ channel for management) is set up and verified is described step by step in
259
+ [SETUP.md](SETUP.md).
260
+
261
+ ## Operation tiers and the operator gate
262
+
263
+ Every tool has a fixed classification, kept in one place in the code
264
+ (`TOOL_TIER`) so this table and the policy cannot drift apart:
265
+
266
+ | Tier | Meaning | Tools |
267
+ |---|---|---|
268
+ | **READ** | only observes, changes nothing | `status`, `models_present`, `job_status`, `list_jobs`, `list_variants`, `show_variant`, `list_adapters`, `training_status`, `check_training_env`, `check_isolation` |
269
+ | **CONTROL** | changes running state reversibly | `fetch_model`, `create_variant`, `switch_service`, `stop_service`, `start_service`, `start_training` |
270
+ | **DESTRUCTIVE** | removes or cancels | `remove_model`, `remove_variant`, `cancel_training` |
271
+
272
+ This classification is **not** an authorization system (see the note in
273
+ [SECURITY.md](SECURITY.md#authorization--scope)); it is a coarse operator switch
274
+ so you can narrow which operations this server will actually run, without editing
275
+ code. A disabled operation stays listed over MCP but is refused on call, before
276
+ any machine access:
277
+
278
+ | Variable | Default | Effect |
279
+ |---|---|---|
280
+ | `MM_MAX_TIER` | `read` | Highest tier the server will run. `read` exposes only READ tools; `control` exposes READ and CONTROL; `destructive` exposes all. |
281
+ | `MM_ENABLED_OPERATIONS` | empty (everything within `MM_MAX_TIER`) | Comma-separated **allowlist** of exact tool names, applied on top of `MM_MAX_TIER`: a tool runs only if it is both within `MM_MAX_TIER` and, when this list is set, named here. A typo in a name stops the server at startup rather than silently exposing more. |
282
+
283
+ **Default is read-only (deny-by-default).** Set nothing and only the READ tools
284
+ run; CONTROL and DESTRUCTIVE operations must be switched on explicitly. The gate
285
+ refuses a disabled operation **early and cleanly**, before any SSH or VM access,
286
+ with a message that names the tier and how to change it. To also allow the
287
+ management operations (`fetch_model`, `switch_service`, training, ...):
288
+
289
+ ```json
290
+ "env": { "MM_MAX_TIER": "control" }
291
+ ```
292
+
293
+ Or expose exactly two operations and nothing else, note that `MM_ENABLED_OPERATIONS`
294
+ narrows further on top of `MM_MAX_TIER`, so the tier must already admit both
295
+ named tools, here `switch_service` is CONTROL, so `MM_MAX_TIER` must be raised
296
+ to at least `control`:
297
+
298
+ ```json
299
+ "env": { "MM_MAX_TIER": "control", "MM_ENABLED_OPERATIONS": "status,switch_service" }
300
+ ```
301
+
302
+ ### Concurrency guard
303
+
304
+ The machine has one GPU and one inference service, so two state changes at once
305
+ would leave it inconsistent. Every GPU-state-changing tool runs its critical
306
+ section serialized (`fetch_model` and `cancel_training` are excepted by design:
307
+ fetch is a background job, and cancel must always get through): while one runs,
308
+ a second is **refused immediately** (not queued), and the GPU-competing operations
309
+ (`switch_service`, `remove_model`, `create_variant`, `start_training`,
310
+ `start_service`) are refused while a training run this server started is still
311
+ active. The guard is in-process: it makes this server's own
312
+ calls consistent. A training run started from a *different* session is caught on
313
+ the machine side (a new `start_training` stops inference first), not fully
314
+ serialized here; that boundary is stated in [SECURITY.md](SECURITY.md).
315
+
316
+ ## The tools
317
+
318
+ | Tool | Purpose |
319
+ |---|---|
320
+ | `status` | What is running: service, model, GPU memory, disk space, containers |
321
+ | `models_present` | Inventory on the machine, split by Ollama and Hugging Face |
322
+ | `fetch_model` | Fetch a model, returns immediately with an id |
323
+ | `job_status` | Progress of a background job |
324
+ | `list_jobs` | Known jobs here and on the machine |
325
+ | `remove_model` | Remove, only with a literal confirmation |
326
+ | `create_variant` | Build a custom Ollama variant: base model, system prompt, parameters |
327
+ | `list_variants` | Which variants exist, where they come from, what is set |
328
+ | `show_variant` | What a variant really contains, from both sources |
329
+ | `remove_variant` | Clear a variant away, only with a literal confirmation |
330
+ | `switch_service` | Switch vLLM to a different model, with estimate and cross-check |
331
+ | `stop_service`, `start_service` | Halt the service and bring it back up |
332
+ | `check_training_env` | Is the LoRA environment present and importable |
333
+ | `start_training` | LoRA run in the background, stops inference first and says so |
334
+ | `training_status` | Step, loss, estimated time remaining |
335
+ | `cancel_training` | End a run, with confirmation |
336
+ | `list_adapters` | Finished adapters with their base model |
337
+ | `check_isolation` | Tunnel works, LAN does not: both by the real behavior |
338
+
339
+ ### Constraints of a machine with one GPU
340
+
341
+ - **Only one large model at a time.** `switch_service` computes beforehand
342
+ whether weights plus key-value cache plus base load fit into the allotted
343
+ memory, and rejects rather than trying. Computed with the real file sizes and
344
+ the model's `config.json`, not with a rule of thumb from the name; only if both
345
+ are missing is there a rough estimate, then explicitly marked as uncertain.
346
+ - **`switch_service` may only start a known container image and mount a known
347
+ path.** The container it (re)creates is checked against a closed image allowlist
348
+ (the configured vLLM image `MM_VLLM_IMAGE`, default `vllm/vllm-openai`, plus
349
+ `ollama/ollama`, optionally with a tag or digest; a mirrored vLLM image must be
350
+ set via `MM_VLLM_IMAGE` or it is rejected) and every bind mount's host side must
351
+ lie under one of the configured data roots; see [SECURITY.md](SECURITY.md) for
352
+ the full reasoning.
353
+ - **Inference and training do not share the card.** `start_training` rejects as
354
+ long as the confirmation to stop inference is not set, and names what would be
355
+ stopped. Its numeric hyper-parameters (epochs, rank, alpha, learning rate,
356
+ max length, batch size, accumulation) each have a fixed range that is checked
357
+ **on both sides**: here before the run is written, and independently again on
358
+ the machine before any value reaches the training library.
359
+ - **The machine does not always have the card.** If it is unreachable, the server
360
+ reports that as a state of its own with an explanation, not as an error. If a
361
+ second machine sharing the GPU is known via `MM_VM2_HOST`, the state is
362
+ `gpu_probably_on_second_machine`, otherwise simply `machine_unreachable`.
363
+ - **A switch that fails silently is the most dangerous failure mode.** After
364
+ every switch it is checked via `/health` and `/v1/models` whether the service
365
+ really answers and serves the desired model. The result is always in the report.
366
+
367
+ ## Custom Ollama variants
368
+
369
+ Ollama lets you derive a model with a fixed system prompt, its own parameters and
370
+ later a trained adapter, via a **Modelfile**. The derivation then appears as its
371
+ own model name and is addressable like any other model. That is the path to
372
+ specialized agents and, at the same time, the path by which training results
373
+ enter operation.
374
+
375
+ ```
376
+ create_variant(
377
+ name = "handyman:v1",
378
+ base_model = "qwen3:4b",
379
+ system_prompt = "You are a terse expert assistant ...",
380
+ parameters = {"num_ctx": 8192, "temperature": 0.2})
381
+ ```
382
+
383
+ ### The delicate spot: free text that runs into a directive file
384
+
385
+ A system prompt is prose the user must be able to determine. But it lands in a
386
+ file Ollama reads as a **directive list**. Passing it through unchecked would
387
+ bypass the whole protection, not over the shell but a layer above it, in the
388
+ Modelfile itself. Four measures interlock:
389
+
390
+ 1. **The Modelfile is assembled on the machine, not transferred.** Only parts go
391
+ over the control channel: base model, an object of parameter name and number,
392
+ an adapter path and the system prompt. There is no way to submit a finished
393
+ Modelfile.
394
+ 2. **The permitted directives are in a closed list.** The template can produce
395
+ `FROM`, `PARAMETER`, `ADAPTER` and `SYSTEM` and nothing else. `FROM` takes an
396
+ Ollama model name and no path and is checked against the actual inventory.
397
+ 3. **The free text travels as Base64.** No shell syntax and no newline can arise
398
+ in transit from the Base64 alphabet; it is unpacked only on the machine,
399
+ straight into the file.
400
+ 4. **The character set of the free text contains no double quote.** A multi-line
401
+ Modelfile value ends with three of them; with the character missing from the
402
+ set, the text cannot end its own block and never become a directive. For
403
+ quotes the typographic quotation marks and the apostrophe are available.
404
+
405
+ Checked on **both** sides, since `validation.py` runs here as on the machine. A
406
+ crafted request placed straight into the control channel, past the check here, is
407
+ rejected by the wrapper on its own.
408
+
409
+ ### The tunable parameters
410
+
411
+ Mapped are the ones actually worth using, not everything Ollama knows. Each has a
412
+ range that is checked before the value reaches the machine.
413
+
414
+ | Parameter | Type | Range | Effect |
415
+ |---|---|---|---|
416
+ | `num_ctx` | integer | 256 to 131072 | how much text the model keeps in view at once |
417
+ | `temperature` | number | 0.0 to 2.0 | randomness; 0 means the most repeatable answer |
418
+ | `top_p` | number | 0.0 to 1.0 | nucleus sampling by probability mass |
419
+ | `top_k` | integer | 1 to 200 | nucleus sampling by count |
420
+ | `min_p` | number | 0.0 to 1.0 | lower bound relative to the most probable token |
421
+ | `repeat_penalty` | number | 0.5 to 2.0 | repetition brake; 1.0 means off |
422
+ | `repeat_last_n` | integer | -1 to 4096 | lookback of the brake; -1 means num_ctx |
423
+ | `num_predict` | integer | -2 to 32768 | answer length; -1 unlimited, -2 until the context is full |
424
+ | `seed` | integer | 0 to 2147483647 | fixed seed for repeatable answers |
425
+
426
+ ### Adapter: prepared but not live
427
+
428
+ The `adapter` parameter is built and checked, but the path has **never run**,
429
+ because there is no trained adapter in the reference state. What is present: the
430
+ allowlist for the adapter path, the conversion to the container's view, the
431
+ `ADAPTER` line in the template and the reading in the display tools. What has to
432
+ be settled with the first real adapter (Safetensors vs. GGUF format, the storage
433
+ location in the container, the fit to base model and quantization) is noted as
434
+ open in the code, in place. Until that is proven: leave `adapter` empty.
435
+
436
+ ## Selftest
437
+
438
+ ```
439
+ .venv/bin/python selftest.py # without the machine, HTTP is mocked
440
+ .venv/bin/python selftest.py --live # additionally against the real machine
441
+ ```
442
+
443
+ Most groups run without a machine; only the optional `--live` group M needs
444
+ one. The operator-side setup scripts (`setup/`, `vm_side/`) that
445
+ some checks exercise ship **with this repo**, next to the server, so with them in
446
+ place the selftest runs additional checks on this machine, no model machine
447
+ required: on top of the server-code groups they cover the wrapper in actual
448
+ execution (group D), the setup script itself (group J), the isolation (group K)
449
+ and the operator policy, the concurrency guard and the isolation helpers (group
450
+ N). Allowlists against an attack battery, catalog and substitution, request
451
+ format, the service side against a stub server, memory estimation against
452
+ hand-checked values, connection layer, parsing of the machine outputs and the
453
+ variants all run and are checked. Should you run the server directory on its own,
454
+ with the `setup/` and `vm_side/` scripts removed from next to it, the selftest
455
+ still passes the server-code groups and skips groups D, J, K
456
+ and the operator-only parts of I and N cleanly with a note, not counted as
457
+ failed. Neither scenario needs a model machine. Only the `--live` group M does: it
458
+ runs against a reachable, set-up machine and is optional on top of either
459
+ scenario. (The exact check count is what the runner prints on the run in front
460
+ of you; it grows as checks are added, so it is not quoted here as a fixed
461
+ number and is not contractual.)
462
+
463
+ `--live` does NOT read `MM_VM_HOST`/`MM_VM_USER`/`MM_CONTAINER_ROOT`; it uses
464
+ its own, separate variables, defaulting to an address that is never reachable
465
+ (`TEST-NET`, RFC 5737) so a plain `--live` run without further setup is
466
+ silently skipped rather than a false pass:
467
+
468
+ | Name | Default | Meaning |
469
+ |---|---|---|
470
+ | `MM_LIVE_HOST` | `192.0.2.10` | Address of the machine group M probes |
471
+ | `MM_LIVE_PORT_VLLM` | `8000` | vLLM port on that machine |
472
+ | `MM_LIVE_PORT_OLLAMA` | `11434` | Ollama port on that machine |
473
+
474
+ Set all three to point `--live` at your actual, reachable machine; otherwise
475
+ you will see "VM not reachable, live group is skipped. Not an error." even
476
+ though `MM_VM_HOST` itself is configured correctly for normal server use.
477
+
478
+ ## Environment variables
479
+
480
+ Required fields are marked **required**; they have no default value.
481
+
482
+ | Name | Default | Meaning |
483
+ |---|---|---|
484
+ | `MM_VM_HOST` | **required** | Address of the model machine (`127.0.0.1`, LAN IP or tunnel/VPN address) |
485
+ | `MM_VM_USER` | **required** | SSH user on the machine |
486
+ | `MM_ACCESS` | `tunnel` | `tunnel` (over the SSH tunnel to `127.0.0.1`) or `direct` (straight to the address) |
487
+ | `MM_MAX_TIER` | `read` | Highest operation tier the server exposes: `read`, `control` or `destructive` (see [Operation tiers](#operation-tiers-and-the-operator-gate)) |
488
+ | `MM_ENABLED_OPERATIONS` | empty | Optional comma-separated allowlist of exact tool names; when set, only those run. A typo aborts startup |
489
+ | `MM_VM2_HOST` | empty | Optional: second machine sharing the GPU, to interpret unreachability. Leave empty if there is only one machine |
490
+ | `MM_KEY_DIR` | `~/.ssh/modelmanager` | Storage of the two SSH keys (outside the repo) |
491
+ | `MM_KEY_DATA` | `${MM_KEY_DIR}/datachannel` | Private key of the data channel |
492
+ | `MM_KEY_CONTROL` | `${MM_KEY_DIR}/controlchannel` | Private key of the control channel |
493
+ | `MM_KNOWN_HOSTS` | `${MM_KEY_DIR}/known_hosts` | Pinned host key file of the model machine |
494
+ | `MM_FROM` | **required** (only while building the level-2 allowlist) | Permitted sender address(es) of the two keys, several comma-separated |
495
+ | `MM_FROM_NET_ALLOWED` | `no` | Set to `yes` to let `MM_FROM` allow a whole network, with a warning (only while building the level-2 allowlist) |
496
+ | `MM_REMOTE_PORT_VLLM` | `8000` | vLLM port on the machine (vLLM's standard port) |
497
+ | `MM_REMOTE_PORT_OLLAMA` | `11434` | Ollama port on the machine (Ollama's standard port) |
498
+ | `MM_LOCAL_PORT_VLLM` | `8000` | Local port under which the tunnel exposes vLLM |
499
+ | `MM_LOCAL_PORT_OLLAMA` | `11434` | Local port under which the tunnel exposes Ollama |
500
+ | `MM_CONTAINER_VLLM` | `vllm` | Name of the vLLM Docker container |
501
+ | `MM_CONTAINER_OLLAMA` | `ollama` | Name of the Ollama Docker container |
502
+ | `MM_VLLM_IMAGE` | `vllm/vllm-openai` | vLLM image the recreate allowlist accepts; set this to a mirrored/renamed image, otherwise it is rejected |
503
+ | `MM_MIN_DISK_GB` | `15` | Lower bound for an Ollama pull |
504
+ | `MM_CONTAINER_ROOT` | **required, no default** | Root directory on the machine under which all model, adapter and cache data live; the server does not start without it |
505
+ | `MM_VM_ROOT` | `${MM_CONTAINER_ROOT}/manager` | This tool's own working root on the machine (job folders live beneath it) |
506
+ | `MM_VM_DATA_ROOT` | `${MM_CONTAINER_ROOT}` | Root a training dataset path must lie under (`data_path_check`, `df` usage report) |
507
+ | `MM_VM_ADAPTER` | `${MM_CONTAINER_ROOT}/adapter` | Where a finished LoRA training run stores its adapter |
508
+ | `MM_LORA_PYTHON` | `${MM_CONTAINER_ROOT}/lora-venv/bin/python` | Interpreter of the LoRA training environment on the machine |
509
+ | `MM_VM_OLLAMA_STORE` | `${MM_CONTAINER_ROOT}/ollama` | Ollama's data folder on the machine (mounted into the container, see table below) |
510
+ | `MM_IN_CONTAINER_STORE` | `/root/.ollama` | The same folder's path AS SEEN INSIDE the Ollama container |
511
+ | `MM_VM_HF_CACHE` | `${MM_CONTAINER_ROOT}/hf` | Root of the Hugging Face weights cache on the machine |
512
+ | `MM_GPU_TOTAL_GB` | `12.0` | Assumed GPU memory in GB, used only when `nvidia-smi`'s real reading fails |
513
+ | `MM_AUDIT` (machine-side, not set here) | `${MM_VM_ROOT}/audit.jsonl` | Path of the audit log on the model machine itself; read there by the machine-side wrapper, defaulting to the `audit.jsonl` that `install.sh` creates. Not read by this server |
514
+ | `MM_ENFORCE_ROOT_OWNED` (machine-side, not set here) | unset | Set to `1` to force the machine-side wrapper's root-owned guard on `lib/` even in a checkout copy where `install.sh` has not flipped it on. Can only turn the guard ON, never off, so the environment cannot weaken an installed copy. Used by the self-test; not read by this server |
515
+ | `MM_STATE_DIR` | `$XDG_STATE_HOME/mcp-modelmanager` (or `~/.local/state/mcp-modelmanager`) | Where this server side stores the local job state (`fetch_model`/`start_training` job files) |
516
+
517
+ The local tunnel ports default to the same well-known values as the remote
518
+ ports; adjust them if those are already taken locally.
519
+
520
+ `MM_CONTAINER_ROOT` is required and has no built-in default (there is no
521
+ default machine layout this tool could assume); without it the server exits
522
+ at startup. The six `MM_VM_*`/`MM_LORA_PYTHON` variables above derive from it
523
+ by default and stay individually overridable, exactly like `MM_IN_CONTAINER_STORE`,
524
+ so a machine with a DIFFERENT directory layout can be adapted through
525
+ configuration alone, no code change needed. Every value is checked at startup
526
+ against a strict allowlist for VM paths (absolute, letters/digits/dot/hyphen/
527
+ underscore/slash only, no `..`, no spaces, no shell characters); a malformed
528
+ value stops the server immediately instead of being trusted. `MM_VM_DATA_ROOT`
529
+ and `MM_VM_OLLAMA_STORE` additionally become the allowlist roots for every path
530
+ a tool call may pass in later (a training dataset path, an adapter path), so
531
+ they stay consistent with what is actually configured.
532
+
533
+ Places on the machine that a variant needs, derived from the roots above (in
534
+ `operations.py`, not separately settable). The setup creates these paths:
535
+
536
+ | On the machine | In the container | For |
537
+ |---|---|---|
538
+ | `MM_VM_OLLAMA_STORE` (default `${MM_CONTAINER_ROOT}/ollama`) | `MM_IN_CONTAINER_STORE` (default `/root/.ollama`) | Ollama's data folder, mounted |
539
+ | `<MM_VM_OLLAMA_STORE>/variants/<name>/Modelfile` | `<MM_IN_CONTAINER_STORE>/variants/...` | the assembled Modelfile |
540
+ | `<MM_VM_OLLAMA_STORE>/adapter` | `<MM_IN_CONTAINER_STORE>/adapter` | storage for adapters |
541
+
542
+ ## License
543
+
544
+ MIT, see [LICENSE](LICENSE).