feat(cli+mcplocal): persistent provider disable/enable
Some checks failed
CI/CD / lint (pull_request) Successful in 55s
CI/CD / test (pull_request) Successful in 1m11s
CI/CD / typecheck (pull_request) Successful in 3m20s
CI/CD / smoke (pull_request) Failing after 52s
CI/CD / build (pull_request) Successful in 3m59s
CI/CD / publish (pull_request) Has been skipped

Adds two new subcommands on top of v7's provider lifecycle CLI:

  mcpctl provider disable vllm-local   # release GPU + survive restart
  mcpctl provider enable  vllm-local   # clear the flag, ready to chat

Use case: vLLM keeps crashing on engine init. `down` works for "now"
but the next chat triggers a restart; `disable` writes
`disabled: true` into the provider's entry in ~/.mcpctl/config.json
and short-circuits complete()/ensureRunning() until you re-enable.

Implementation:
- LlmProviderEntry / LlmProviderFileEntry: new optional `disabled` field
- ManagedVllmProvider: setDisabled(bool), isDisabled(), gate in
  complete()/ensureRunning(), expose `disabled` in getStatus()
- mcplocal HTTP: POST /llm/providers/:name/{disable,enable} write the
  config file and apply the change live; /start returns 409 when the
  target is disabled instead of silently failing
- Boot: createSingleProvider honors `entry.disabled` so a known-bad
  vLLM doesn't auto-start on the first chat after mcplocal restart
- CLI: `disable` / `enable` subcommands on `mcpctl provider`; status
  output now shows `(disabled)` next to the state

`enable` is live — provider stays in the registry while disabled, so
flipping the flag back is enough; no mcplocal restart needed.

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-05-03 15:57:01 +01:00
parent fe27947f80
commit d04adb5623
8 changed files with 211 additions and 14 deletions

View File

@@ -1,14 +1,22 @@
/**
* `mcpctl provider <name> <up|down|status>`
* `mcpctl provider <action> <name>` — managed LLM lifecycle control.
*
* Lifecycle control for managed local LLM providers (vllm-managed). Talks
* to mcplocal's `/llm/providers/:name/{status,start,stop}` HTTP endpoints
* — non-managed providers (anthropic, openai, gemini-cli) get a clear
* error rather than a no-op.
* up — warmup() now (next chat is fast)
* down — dispose() now; auto-restarts on next chat
* status — state, pid, uptime, disabled flag
* disable — dispose() AND set `disabled: true` in ~/.mcpctl/config.json;
* survives mcplocal restarts; complete()/ensureRunning() short-
* circuit so the GPU process doesn't spawn until you `enable`
* enable — clear the disabled flag (live + on disk)
*
* Practical use: `mcpctl provider vllm-local down` to release GPU memory
* without restarting mcplocal (which would drop the SSE connection to mcpd
* and re-publish all virtual Llms).
* Talks to mcplocal's `/llm/providers/:name/{status,start,stop,enable,disable}`
* HTTP endpoints. Non-managed providers (anthropic, openai, gemini-cli)
* get a clear 400 rather than a no-op for the lifecycle actions.
*
* Practical use:
* `mcpctl provider down vllm-local` — release GPU memory now
* `mcpctl provider disable vllm-local` — release GPU AND prevent auto-start
* (e.g. when vLLM keeps crashing)
*/
import { Command } from 'commander';
import http from 'node:http';
@@ -25,6 +33,10 @@ interface ProviderStatusResponse {
lastError?: string | null;
pid?: number | null;
uptime?: number | null;
/** True when the persistent disable flag is set (config + live). */
disabled?: boolean;
/** Set by /enable + /disable — true if the config file was rewritten. */
persisted?: boolean;
}
interface ErrorResponse {
@@ -54,7 +66,8 @@ function formatStatus(s: ProviderStatusResponse): string {
if (!s.managed) {
return `${s.name}: unmanaged (no lifecycle — API-key or remote provider)`;
}
const lines = [`${s.name}: ${s.state ?? 'unknown'}`];
const stateLabel = s.disabled === true ? `${s.state ?? 'stopped'} (disabled)` : (s.state ?? 'unknown');
const lines = [`${s.name}: ${stateLabel}`];
if (s.pid !== null && s.pid !== undefined) lines.push(` pid: ${String(s.pid)}`);
if (s.uptime !== null && s.uptime !== undefined) {
const sec = s.uptime;
@@ -126,5 +139,47 @@ export function createProviderCommand(deps: ProviderCommandDeps): Command {
deps.log(`${status.name}: ${status.state ?? 'stopped'} (GPU released — next chat will trigger restart)`);
});
cmd
.command('disable')
.description('Persistently disable a managed provider (survives mcplocal restart)')
.argument('<name>', 'Provider name (e.g. vllm-local)')
.action(async (name: string) => {
const res = await fetchJson<ProviderStatusResponse | ErrorResponse>(
`${mcplocalUrl}/llm/providers/${encodeURIComponent(name)}/disable`,
'POST',
);
if (res.status !== 200) {
deps.log(`error: ${(res.body as ErrorResponse).error}`);
process.exitCode = 1;
return;
}
const status = res.body as ProviderStatusResponse;
const persistedNote = status.persisted === true
? ' (saved to ~/.mcpctl/config.json — survives restart)'
: ' (live only — provider is not in config file, restart will undo)';
deps.log(`${status.name}: disabled${persistedNote}`);
});
cmd
.command('enable')
.description('Re-enable a previously-disabled provider')
.argument('<name>', 'Provider name (e.g. vllm-local)')
.action(async (name: string) => {
const res = await fetchJson<ProviderStatusResponse | ErrorResponse>(
`${mcplocalUrl}/llm/providers/${encodeURIComponent(name)}/enable`,
'POST',
);
if (res.status !== 200) {
deps.log(`error: ${(res.body as ErrorResponse).error}`);
process.exitCode = 1;
return;
}
const status = res.body as ProviderStatusResponse;
const persistedNote = status.persisted === true
? ' (saved to ~/.mcpctl/config.json)'
: '';
deps.log(`${status.name}: enabled${persistedNote} — next chat will start it`);
});
return cmd;
}

View File

@@ -46,6 +46,9 @@ export const LlmProviderEntrySchema = z.object({
idleTimeoutMinutes: z.number().int().positive().optional(),
/** vllm-managed: extra args for `vllm serve` */
extraArgs: z.array(z.string()).optional(),
/** When true, mcplocal keeps the provider registered but suppresses
* auto-start. Toggle via `mcpctl provider {enable,disable} <name>`. */
disabled: z.boolean().optional(),
}).strict();
export type LlmProviderEntry = z.infer<typeof LlmProviderEntrySchema>;