feat(ql3): harden copilot mcp host deployment

This commit is contained in:
whyour
2026-08-16 03:41:38 +08:00
parent 58025ede55
commit da4e7cf688
25 changed files with 734 additions and 48 deletions
@@ -2,14 +2,46 @@
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { probeClusterCopilotClientReadiness } from '../copilot-client/client';
import { readClusterCopilotMcpServerConfig } from './config';
import { createQingLongClusterCopilotMcpServer } from './server';
const USAGE = 'Usage: ql3-copilot-mcp --config /absolute/private-config.json';
const USAGE = [
'Usage: ql3-copilot-mcp --config /absolute/private-config.json [--concurrency-ceiling=1..16]',
' ql3-copilot-mcp --check --config /absolute/private-config.json [--concurrency-ceiling=1..16]',
].join('\n');
function configArgument(argv: readonly string[]): string | null {
if (argv.length !== 2 || argv[0] !== '--config' || !argv[1]) return null;
return argv[1];
interface ClusterCopilotMcpCliArguments {
readonly check: boolean;
readonly configFile: string;
readonly concurrencyCeiling: number;
}
function configArgument(
argv: readonly string[],
): Readonly<ClusterCopilotMcpCliArguments> | null {
const check = argv[0] === '--check';
const offset = check ? 1 : 0;
if (
(argv.length !== offset + 2 && argv.length !== offset + 3) ||
argv[offset] !== '--config' ||
!argv[offset + 1]
) {
return null;
}
let concurrencyCeiling = 16;
if (argv.length === offset + 3) {
const match = /^--concurrency-ceiling=([1-9]|1[0-6])$/u.exec(
argv[offset + 2] ?? '',
);
if (!match) return null;
concurrencyCeiling = Number(match[1]);
}
return Object.freeze({
check,
configFile: argv[offset + 1]!,
concurrencyCeiling,
});
}
function fact(event: 'process_failed' | 'transport_error'): string {
@@ -26,8 +58,8 @@ async function main(argv: readonly string[]): Promise<void> {
process.stdout.write(`${USAGE}\n`);
return;
}
const configFile = configArgument(argv);
if (configFile === null) {
const command = configArgument(argv);
if (command === null) {
process.stderr.write(
`${JSON.stringify({
code: 'QL3_CLUSTER_COPILOT_MCP_CLI_USAGE_INVALID',
@@ -46,7 +78,33 @@ async function main(argv: readonly string[]): Promise<void> {
};
try {
const config = readClusterCopilotMcpServerConfig(configFile);
const config = readClusterCopilotMcpServerConfig(command.configFile);
if (config.maxConcurrentRequests > command.concurrencyCeiling) {
throw new TypeError('Cluster Copilot MCP concurrency ceiling exceeded');
}
if (command.check) {
const readiness = await probeClusterCopilotClientReadiness(
config.clientConfigFile,
);
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-cluster-copilot-mcp',
event: 'preflight_checked',
transport: readiness.transport,
ready: readiness.ready,
configuration: 'valid',
credential: 'valid',
maxConcurrentRequests: config.maxConcurrentRequests,
concurrencyCeiling: command.concurrencyCeiling,
requestMethod: 'GET',
requestPath: '/readyz',
mutation: false,
})}\n`,
);
if (!readiness.ready) process.exitCode = 69;
return;
}
handle = serveStdio(
() => createQingLongClusterCopilotMcpServer({ config }),
{
@@ -40,6 +40,12 @@ export const QINGLONG3_CLUSTER_PRODUCT_COMMANDS: readonly QingLong3ClusterProduc
target: 'copilot-client/cli.js',
description: 'diagnose, inspect, read or cancel Runs through the API',
}),
Object.freeze({
name: 'copilot-mcp',
binary: 'ql3-copilot-mcp',
target: 'copilot-mcp/cli.js',
description: 'serve the bounded Cluster Copilot MCP over stdio',
}),
Object.freeze({
name: 'package',
binary: 'ql3-plugin-package-client',
@@ -177,7 +183,7 @@ export function qingLong3ClusterProductHelp(): string {
return [
'Usage: ql3-cluster-admin <command> [arguments]',
'',
'Remote client commands:',
'Cluster product commands:',
commands,
'',
'Local operator commands:',
@@ -185,7 +191,8 @@ export function qingLong3ClusterProductHelp(): string {
' context probe --context=/absolute/operator-context.json',
'',
'Use `ql3-cluster-admin <command> --help` for command-specific usage.',
'Use `--context=/absolute/operator-context.json` to inject only stable client paths.',
'Use `--context=/absolute/operator-context.json` only with remote client commands.',
'Keep the MCP config explicit; it contains stable paths to a separately rotated credential.',
'Command and short-lived assertion files always remain explicit per invocation.',
'Server, migration, recovery, executor and key-custody authorities remain isolated.',
].join('\n');