chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/**
* Environment utilities for child processes
*/
/**
* Build a clean env for child processes (strips ELECTRON_RUN_AS_NODE)
*/
export function buildBaseEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) {
env[key] = value;
}
}
// Child MCP servers should run normally, not as Electron Node.js instances
delete env.ELECTRON_RUN_AS_NODE;
return env;
}

View File

@@ -0,0 +1,21 @@
/**
* HTTP headers utilities
*/
import type { StreamableServerEntry, SseServerEntry } from '../types.js';
/**
* Build HTTP headers from entry config (merge headers + authToken)
*/
export function buildRequestHeaders(
entry: StreamableServerEntry | SseServerEntry,
): Record<string, string> | undefined {
const headers: Record<string, string> = {};
if (entry.headers) {
Object.assign(headers, entry.headers);
}
if (entry.authToken) {
headers['Authorization'] = `Bearer ${entry.authToken}`;
}
return Object.keys(headers).length > 0 ? headers : undefined;
}

View File

@@ -0,0 +1,290 @@
/**
* Transport layer — connect to upstream MCP servers via stdio, Streamable HTTP, or SSE
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { CustomStdioClientTransport } from '../customStdio.js';
import type { StdioServerEntry, StreamableServerEntry, SseServerEntry } from '../types.js';
import { logError, logInfo, logWarn } from '../logger.js';
import { ResilientTransportWrapper } from '../resilient.js';
// Re-export sub-modules
export { buildBaseEnv } from './env.js';
export { buildRequestHeaders } from './headers.js';
export type { ConnectedClient } from './types.js';
const DEFAULT_STDIO_CONNECTION_TIMEOUT_MS = 60_000;
// HTTP timeout reduced to 20s to avoid race with qimingcode's 30s session/new timeout
// When a server fails (e.g., auth error), ResilientTransportWrapper retries indefinitely,
// but this timeout ensures we fail fast and let other servers load.
const DEFAULT_HTTP_CONNECTION_TIMEOUT_MS = 20_000;
/**
* Helper to wrap a promise with a timeout
*/
async function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
let timeoutId: ReturnType<typeof setTimeout>;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(message)), ms);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutId!);
}
}
/**
* Connect to a stdio MCP server (spawn child process)
*/
export async function connectStdio(
id: string,
entry: StdioServerEntry,
baseEnv: Record<string, string>,
): Promise<import('./types.js').ConnectedClient> {
logInfo(`Connecting to "${id}" (stdio): ${entry.command} ${(entry.args || []).join(' ')}`);
const wrapper = new ResilientTransportWrapper({
name: id,
connectParams: async () => {
const t = new CustomStdioClientTransport({
command: entry.command,
args: entry.args || [],
env: { ...baseEnv, ...(entry.env || {}) },
stderr: 'pipe',
});
if (t.stderr) {
t.stderr.on('data', (chunk: Buffer) => {
const text = chunk.toString().trim();
if (text) {
process.stderr.write(`[child:${id}] ${text}\n`);
}
});
}
return t;
},
// No heartbeat for stdio — child process close/error events handle detection
pingIntervalMs: 0,
});
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
// Set reconnect handler to re-establish MCP session via client.connect()
// Note: We need to clear the SDK's internal _transport reference without closing it.
// client.close() would close the wrapper transport, so we manually clear the reference.
wrapper.onreconnect = async () => {
logInfo(`[${id}] Reconnecting MCP session...`);
// Clear SDK's transport reference without closing it (wrapper is already reconnected)
(client as any)._transport = undefined;
await client.connect(wrapper);
logInfo(`[${id}] MCP session reconnected`);
};
const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_STDIO_CONNECTION_TIMEOUT_MS;
try {
await withTimeout(
(async () => {
await wrapper.start();
await client.connect(wrapper);
})(),
timeoutMs,
`Connection initialization timed out after ${timeoutMs / 1000}s`
);
} catch (err) {
try { await wrapper.close(); } catch { /* ignore */ }
throw err;
}
return {
client,
transport: wrapper,
cleanup: async () => {
try { await wrapper.close(); } catch { /* ignore */ }
},
};
}
/**
* Connect to a Streamable HTTP MCP server
*
* Supports PersistentMcpBridge endpoints and any remote MCP service using
* the Streamable HTTP transport protocol.
*/
export async function connectStreamable(
id: string,
entry: StreamableServerEntry,
): Promise<import('./types.js').ConnectedClient> {
logInfo(`Connecting to "${id}" (streamable-http): ${entry.url}`);
const { buildRequestHeaders } = await import('./headers.js');
const headers = buildRequestHeaders(entry);
const url = new URL(entry.url);
const wrapper = new ResilientTransportWrapper({
name: id,
connectParams: async () => {
return new StreamableHTTPClientTransport(
url,
headers ? { requestInit: { headers } } : undefined,
);
},
// Use provided interval or default (30s)
pingIntervalMs: entry.pingIntervalMs,
pingTimeoutMs: entry.pingTimeoutMs,
});
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
// Set health check function to use ping() for HTTP-based transports
// ping() is lighter than listTools() and sufficient for connection health
wrapper.setHealthCheckFn(async () => {
try {
const { tools } = await client.listTools();
logInfo(`[proxy-${id}] Streamable HTTP health check OK: ${tools?.length ?? 0} tools`);
return true;
} catch(error) {
logError(`[proxy-${id}] Streamable HTTP health check failed: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
});
// Set reconnect handler to re-establish MCP session via client.connect()
// Note: We need to clear the SDK's internal _transport reference without closing it.
// client.close() would close the wrapper transport, so we manually clear the reference.
// Note: Do NOT call enableHeartbeat() here - performConnect will call startHeartbeat()
// after flushQueue() completes, ensuring health checks use the correct session.
wrapper.onreconnect = async () => {
logInfo(`[${id}] Reconnecting MCP session...`);
// Clear SDK's transport reference without closing it (wrapper is already reconnected)
(client as any)._transport = undefined;
await client.connect(wrapper);
// Note: heartbeat is restarted by ResilientTransport.performConnect after flushQueue()
logInfo(`[${id}] MCP session reconnected`);
};
const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
try {
await withTimeout(
(async () => {
await wrapper.start();
await client.connect(wrapper);
wrapper.enableHeartbeat(); // Start heartbeat AFTER MCP initialize completes
})(),
timeoutMs,
`Connection initialization timed out after ${timeoutMs / 1000}s`
);
} catch (err) {
try { await wrapper.close(); } catch { /* ignore */ }
throw err;
}
return {
client,
transport: wrapper,
cleanup: async () => {
try { await wrapper.close(); } catch { /* ignore */ }
},
};
}
/**
* Connect to an SSE MCP server
*
* Uses the legacy SSE (Server-Sent Events) transport for MCP servers
* that don't support the newer Streamable HTTP protocol.
*/
export async function connectSse(
id: string,
entry: SseServerEntry,
): Promise<import('./types.js').ConnectedClient> {
logInfo(`Connecting to "${id}" (sse): ${entry.url}`);
const { buildRequestHeaders } = await import('./headers.js');
const headers = buildRequestHeaders(entry);
const url = new URL(entry.url);
const wrapper = new ResilientTransportWrapper({
name: id,
connectParams: async () => {
return new SSEClientTransport(
url,
headers ? { requestInit: { headers } } : undefined,
);
},
// Use provided interval or default (30s)
pingIntervalMs: entry.pingIntervalMs,
pingTimeoutMs: entry.pingTimeoutMs,
});
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
// Set health check function to use ping() for HTTP-based transports
// ping() is lighter than listTools() and sufficient for connection health
wrapper.setHealthCheckFn(async () => {
try {
// Get current session_id from transport
const innerTransport = (wrapper as any).activeTransport;
const endpoint = innerTransport?._endpoint?.href || innerTransport?._endpoint || 'no-endpoint';
const { tools } = await client.listTools();
logInfo(`[proxy-${id}] SSE health check OK: ${Array.isArray(tools) ? tools.length : 0} tools (endpoint: ${endpoint})`);
return true;
} catch(error) {
logError(`[proxy-${id}] SSE health check failed: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
});
// Set reconnect handler to re-establish MCP session via client.connect()
// Note: We need to clear the SDK's internal _transport reference without closing it.
// client.close() would close the wrapper transport, so we manually clear the reference.
// Note: Do NOT call enableHeartbeat() here - performConnect will call startHeartbeat()
// after flushQueue() completes, ensuring health checks use the correct session.
wrapper.onreconnect = async () => {
// Get new session_id from transport
const innerTransport = (wrapper as any).activeTransport;
const endpoint = innerTransport?._endpoint?.href || innerTransport?._endpoint || 'no-endpoint';
logInfo(`[${id}] Reconnecting MCP session (endpoint: ${endpoint})...`);
// Clear SDK's transport reference without closing it (wrapper is already reconnected)
(client as any)._transport = undefined;
await client.connect(wrapper);
// Note: heartbeat is restarted by ResilientTransport.performConnect after flushQueue()
logInfo(`[${id}] MCP session reconnected (endpoint: ${endpoint})`);
};
const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
try {
await withTimeout(
(async () => {
await wrapper.start();
await client.connect(wrapper);
wrapper.enableHeartbeat(); // Start heartbeat AFTER MCP initialize completes
})(),
timeoutMs,
`Connection initialization timed out after ${timeoutMs / 1000}s`
);
} catch (err) {
try { await wrapper.close(); } catch { /* ignore */ }
throw err;
}
return {
client,
transport: wrapper,
cleanup: async () => {
try { await wrapper.close(); } catch { /* ignore */ }
},
};
}
/** @deprecated Use connectStreamable instead */
export const connectBridge = connectStreamable;
/** @deprecated Use connectStreamable instead */
export const connectHttp = connectStreamable;

View File

@@ -0,0 +1,12 @@
/**
* Transport types
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
export interface ConnectedClient {
client: Client;
cleanup: () => Promise<void>;
transport: Transport;
}