The daemon host never passed allowCachedAuth when calling runtime.callTool() or runtime.listTools(), and the KeepAliveRuntime callTool wrapper did not forward it to the daemon client either. Without allowCachedAuth, createClientContext skips applyCachedAuthIfAvailable, so cached OAuth tokens are never read and every daemon-managed OAuth server fails after token expiry. Changes: - protocol.ts: add allowCachedAuth to CallToolParams - host.ts: pass allowCachedAuth in callTool and listTools handlers - runtime-wrapper.ts: forward allowCachedAuth in callTool daemon path Fixes openclaw/mcporter#181 Related openclaw/mcporter#179
169 lines
5.4 KiB
TypeScript
169 lines
5.4 KiB
TypeScript
import type { ListResourcesRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
import type { ServerDefinition } from '../config.js';
|
|
import { isKeepAliveServer } from '../lifecycle.js';
|
|
import type { CallOptions, ListToolsOptions, Runtime } from '../runtime.js';
|
|
import type { DaemonClient } from './client.js';
|
|
|
|
interface KeepAliveRuntimeOptions {
|
|
readonly daemonClient: DaemonClient | null;
|
|
readonly keepAliveServers: Set<string>;
|
|
}
|
|
|
|
export function createKeepAliveRuntime(base: Runtime, options: KeepAliveRuntimeOptions): Runtime {
|
|
if (!options.daemonClient || options.keepAliveServers.size === 0) {
|
|
return base;
|
|
}
|
|
return new KeepAliveRuntime(base, options.daemonClient, options.keepAliveServers);
|
|
}
|
|
|
|
class KeepAliveRuntime implements Runtime {
|
|
private readonly restartPromises = new Map<string, Promise<void>>();
|
|
|
|
constructor(
|
|
private readonly base: Runtime,
|
|
private readonly daemon: DaemonClient,
|
|
private readonly keepAliveServers: Set<string>
|
|
) {}
|
|
|
|
listServers(): string[] {
|
|
return this.base.listServers();
|
|
}
|
|
|
|
getDefinitions(): ServerDefinition[] {
|
|
return this.base.getDefinitions();
|
|
}
|
|
|
|
getDefinition(server: string): ServerDefinition {
|
|
return this.base.getDefinition(server);
|
|
}
|
|
|
|
registerDefinition(definition: ServerDefinition, options?: { overwrite?: boolean }): void {
|
|
this.base.registerDefinition(definition, options);
|
|
if (isKeepAliveServer(definition)) {
|
|
this.keepAliveServers.add(definition.name);
|
|
} else {
|
|
this.keepAliveServers.delete(definition.name);
|
|
}
|
|
}
|
|
|
|
async getInstructions(server: string): Promise<string | undefined> {
|
|
return this.base.getInstructions?.(server);
|
|
}
|
|
|
|
async listTools(server: string, options?: ListToolsOptions): Promise<Awaited<ReturnType<Runtime['listTools']>>> {
|
|
if (options?.oauthSessionOptions) {
|
|
return this.base.listTools(server, options);
|
|
}
|
|
if (this.shouldUseDaemon(server)) {
|
|
return (await this.invokeWithRestart(server, 'listTools', () =>
|
|
this.daemon.listTools({
|
|
server,
|
|
includeSchema: options?.includeSchema,
|
|
autoAuthorize: options?.autoAuthorize,
|
|
})
|
|
)) as Awaited<ReturnType<Runtime['listTools']>>;
|
|
}
|
|
return this.base.listTools(server, options);
|
|
}
|
|
|
|
async callTool(server: string, toolName: string, options?: CallOptions): Promise<unknown> {
|
|
if (this.shouldUseDaemon(server)) {
|
|
return this.invokeWithRestart(server, 'callTool', () =>
|
|
this.daemon.callTool({
|
|
server,
|
|
tool: toolName,
|
|
args: options?.args,
|
|
timeoutMs: options?.timeoutMs,
|
|
allowCachedAuth: options?.allowCachedAuth ?? true,
|
|
})
|
|
);
|
|
}
|
|
return this.base.callTool(server, toolName, options);
|
|
}
|
|
|
|
async listResources(server: string, options?: Partial<ListResourcesRequest['params']>): Promise<unknown> {
|
|
if (this.shouldUseDaemon(server)) {
|
|
return this.invokeWithRestart(server, 'listResources', () =>
|
|
this.daemon.listResources({ server, params: options ?? {} })
|
|
);
|
|
}
|
|
return this.base.listResources(server, options);
|
|
}
|
|
|
|
async readResource(server: string, uri: string): Promise<unknown> {
|
|
if (this.shouldUseDaemon(server)) {
|
|
return this.invokeWithRestart(server, 'readResource', () => this.daemon.readResource({ server, uri }));
|
|
}
|
|
return this.base.readResource(server, uri);
|
|
}
|
|
|
|
async connect(server: string): Promise<Awaited<ReturnType<Runtime['connect']>>> {
|
|
return this.base.connect(server);
|
|
}
|
|
|
|
async close(server?: string): Promise<void> {
|
|
if (!server) {
|
|
await this.base.close();
|
|
return;
|
|
}
|
|
if (this.shouldUseDaemon(server)) {
|
|
await this.daemon.closeServer({ server }).catch(() => {});
|
|
return;
|
|
}
|
|
await this.base.close(server);
|
|
}
|
|
|
|
private shouldUseDaemon(server: string): boolean {
|
|
return this.keepAliveServers.has(server);
|
|
}
|
|
|
|
private async invokeWithRestart<T>(server: string, operation: string, action: () => Promise<T>): Promise<T> {
|
|
try {
|
|
return await action();
|
|
} catch (error) {
|
|
if (!shouldRestartDaemonServer(error)) {
|
|
throw error;
|
|
}
|
|
// The daemon keeps STDIO transports warm; if a call fails due to a fatal error,
|
|
// force-close the cached server so the retry launches a fresh Chrome instance.
|
|
logDaemonRetry(server, operation, error);
|
|
await this.restartServer(server);
|
|
return action();
|
|
}
|
|
}
|
|
|
|
private async restartServer(server: string): Promise<void> {
|
|
const existing = this.restartPromises.get(server);
|
|
if (existing) {
|
|
await existing;
|
|
return;
|
|
}
|
|
|
|
const restart = this.daemon.closeServer({ server }).catch(() => {});
|
|
this.restartPromises.set(server, restart);
|
|
try {
|
|
await restart;
|
|
} finally {
|
|
this.restartPromises.delete(server);
|
|
}
|
|
}
|
|
}
|
|
|
|
const NON_FATAL_CODES = new Set([ErrorCode.InvalidRequest, ErrorCode.MethodNotFound, ErrorCode.InvalidParams]);
|
|
|
|
function shouldRestartDaemonServer(error: unknown): boolean {
|
|
if (!error) {
|
|
return false;
|
|
}
|
|
if (error instanceof McpError) {
|
|
return !NON_FATAL_CODES.has(error.code);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function logDaemonRetry(server: string, operation: string, error: unknown): void {
|
|
const reason = error instanceof Error ? error.message : String(error);
|
|
console.error(`[mcporter] Restarting '${server}' before retrying ${operation}: ${reason}`);
|
|
}
|