feat(cli+mcplocal): mcpctl provider <name> {up,down,status} for managed LLMs
Some checks failed
CI/CD / typecheck (pull_request) Successful in 57s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / lint (pull_request) Successful in 3m1s
CI/CD / smoke (pull_request) Failing after 1m47s
CI/CD / build (pull_request) Successful in 5m58s
CI/CD / publish (pull_request) Has been skipped

Adds lifecycle control for managed local LLM providers (vllm-managed)
without the nuclear option of restarting mcplocal. Practical use:

  mcpctl provider vllm-local down    # release GPU memory now
  mcpctl provider vllm-local up      # warm up before the next chat
  mcpctl provider vllm-local status  # see state, pid, uptime

mcplocal exposes three new endpoints:

  GET  /llm/providers/:name/status   → returns lifecycle state for
                                       managed providers, { managed: false }
                                       for unmanaged (anthropic, openai, …)
  POST /llm/providers/:name/start    → calls warmup() (202 + initial state)
  POST /llm/providers/:name/stop     → calls dispose() (200 + post-stop state)

Stop and start return 400 for non-managed providers — stopping an API-key
provider is meaningless. The CLI surfaces the error verbatim.

Restarting mcplocal would also free the GPU but drops the SSE connection
to mcpd and forces every virtual Llm to re-publish; this is the targeted,
non-disruptive escape hatch.

The completions test gained a `topLevelMarkers` filter so a sub-command
named `status` (under `provider`) doesn't trip the existing "non-project
commands must guard with __mcpctl_has_project" rule.

Tests: cli 437/437, mcplocal 731/731.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-29 15:58:46 +01:00
parent 3071bcee8e
commit 356cbe87b5
6 changed files with 234 additions and 3 deletions

View File

@@ -120,7 +120,16 @@ describe('fish completions', () => {
it('non-project commands do not show with --project', () => {
const nonProjectCmds = ['status', 'login', 'logout', 'config', 'apply', 'backup'];
const lines = fishFile.split('\n').filter((l) => l.startsWith('complete') && l.includes('-a '));
// Only check top-level command lines — those are the ones whose
// visibility is gated on `__mcpctl_has_project`. Lines scoped to a
// sub-command (e.g. `provider status`) live under a different
// `__fish_seen_subcommand_from <parent>` predicate and don't need
// the project guard.
const topLevelMarkers = ['$commands', '$project_commands'];
const lines = fishFile.split('\n').filter((l) => {
if (!l.startsWith('complete') || !l.includes('-a ')) return false;
return topLevelMarkers.some((m) => l.includes(m));
});
for (const cmd of nonProjectCmds) {
const cmdLines = lines.filter((l) => {