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
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user