feat(mcplocal+mcpd): wake-recipe config + wake-task execution (v2 Stage 1)

First half of v2 — mcplocal can now declare a hibernating backend and
respond to a `wake` task by running a configured recipe. v2 Stage 2
will wire mcpd to dispatch the wake task before relaying inference.

Config (LlmProviderFileEntry):
- New \`wake\` block on a published provider:
    wake:
      type: http        # or: command
      url: ...           # http only
      method: POST       # http only, default POST
      headers: {...}     # http only
      body: ...          # http only
      command: ...       # command only
      args: [...]        # command only
      maxWaitSeconds: 60 # how long to poll isAvailable() after wake fires

Registrar (mcplocal):
- At publish time, providers with a wake recipe whose isAvailable()
  returns false report initialStatus=hibernating to mcpd. Without a
  wake recipe (legacy v1) or when already up, status stays active.
- handleWakeTask: runs the recipe (HTTP request OR child-process
  spawn), then polls isAvailable() up to maxWaitSeconds, sending a
  heartbeat each loop so mcpd's GC sweep doesn't time us out
  mid-boot. Reports { ok, ms } on success or { error } on
  timeout/recipe failure via the existing _provider-task/:id/result.
- Replaces the v1 stub that rejected wake tasks with "not implemented".

mcpd VirtualLlmService:
- RegisterProviderInput gains optional initialStatus ('active' |
  'hibernating'). The register/upsert path uses it for both new and
  reconnecting rows. Defaults to 'active' so v1 publishers still
  work unchanged.
- Provider-register route's coercer accepts the new field.

Tests: 3 new in registrar.test.ts cover initialStatus selection
(hibernating when wake configured + unavailable, active otherwise,
active when no wake even if unavailable). 8/8 registrar tests, 833/833
mcpd unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-27 15:15:46 +01:00
parent 700d1683c2
commit af0fabd84f
6 changed files with 278 additions and 12 deletions

View File

@@ -37,6 +37,15 @@ export interface RegisterProviderInput {
tier?: string;
description?: string;
extraConfig?: Record<string, unknown>;
/**
* Optional. Lets the publisher hint that the underlying backend is
* asleep — mcpd records the row as `hibernating` and will dispatch a
* `wake` task before any inference. Defaults to `active` (today's
* behavior). v2 publishers (mcplocal with a configured wake recipe)
* pass 'hibernating' when `LlmProvider.isAvailable()` returns false at
* publish time.
*/
initialStatus?: 'active' | 'hibernating';
}
export interface RegisterResult {
@@ -112,6 +121,7 @@ export class VirtualLlmService implements IVirtualLlmService {
const llms: Llm[] = [];
for (const p of input.providers) {
const initialStatus = p.initialStatus ?? 'active';
const existing = await this.repo.findByName(p.name);
if (existing === null) {
const created = await this.repo.create({
@@ -123,7 +133,7 @@ export class VirtualLlmService implements IVirtualLlmService {
...(p.extraConfig !== undefined ? { extraConfig: p.extraConfig } : {}),
kind: 'virtual',
providerSessionId: sessionId,
status: 'active',
status: initialStatus,
lastHeartbeatAt: now,
inactiveSince: null,
});
@@ -156,7 +166,7 @@ export class VirtualLlmService implements IVirtualLlmService {
...(p.extraConfig !== undefined ? { extraConfig: p.extraConfig } : {}),
kind: 'virtual',
providerSessionId: sessionId,
status: 'active',
status: initialStatus,
lastHeartbeatAt: now,
inactiveSince: null,
});