feat(cli)+fix(mcpd): server-side LLM status + SPA fallback 500
Some checks failed
CI/CD / typecheck (pull_request) Successful in 58s
CI/CD / test (pull_request) Successful in 1m9s
CI/CD / lint (pull_request) Successful in 2m14s
CI/CD / smoke (pull_request) Failing after 1m39s
CI/CD / build (pull_request) Successful in 2m14s
CI/CD / publish (pull_request) Has been skipped

Two related fixes:

1. \`mcpctl status\` now lists mcpd-managed Llm rows (the ones created via
   \`mcpctl create llm\`) under a new "Server LLMs:" section, grouped by
   tier with type, model, upstream URL, and key reference. JSON/YAML
   output gains a \`serverLlms\` array.

   Bearer token (from \`mcpctl auth login\` / saved credentials) is
   passed through; if mcpd is unreachable or returns non-200 the
   section is silently omitted (the existing mcpd connectivity line
   already conveys that). 6 new tests cover happy path, empty list,
   token plumbing, and JSON shape.

2. SPA fallback at \`/ui/<deeplink>\` was returning 500 because we
   registered \`@fastify/static\` with \`decorateReply: false\` and then
   called \`reply.sendFile\`. Read index.html once at startup and serve
   it with \`reply.send(html)\` instead — also dodges a per-request
   stat call. Drop \`decorateReply: false\` so future code can use
   reply.sendFile if it ever needs to.

Full suite: 2005/2005 across 149 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-27 11:27:45 +01:00
parent 899f2c750c
commit 0db37e92a4
3 changed files with 189 additions and 7 deletions

View File

@@ -13,7 +13,7 @@
* index.html so client-side react-router routes work on direct hits.
*/
import path from 'node:path';
import { existsSync, statSync } from 'node:fs';
import { existsSync, statSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import type { FastifyInstance } from 'fastify';
import fastifyStatic from '@fastify/static';
@@ -57,13 +57,27 @@ export async function registerWebUi(app: FastifyInstance): Promise<void> {
root,
prefix: '/ui/',
wildcard: false,
decorateReply: false,
});
// SPA fallback — react-router URLs like /ui/agents/foo/personalities/bar
// need index.html to bootstrap the app.
// Read index.html once at startup; the SPA fallback below serves it
// verbatim for every unmatched /ui/* path so client-side routing works
// on direct hits. Reading once also dodges a per-request `sendFile`
// call — there's only one file ever served from this handler.
const indexHtmlPath = path.join(root, 'index.html');
const indexHtml = existsSync(indexHtmlPath)
? readFileSync(indexHtmlPath, 'utf-8')
: null;
if (indexHtml === null) {
app.log.warn({ root }, 'web UI index.html missing; deep links to /ui/<path> will 404');
}
app.get('/ui/*', (_request, reply) => {
return reply.sendFile('index.html', root);
if (indexHtml === null) {
reply.code(404);
return { error: 'index.html missing from web UI bundle' };
}
reply.type('text/html').send(indexHtml);
return reply;
});
// Cover the bare /ui (no trailing slash) too.
app.get('/ui', (_request, reply) => {