← Files NGS Analysis WorkbenchARCHIVED FILE
src/mcp/client.ts
3.61 KB · Oct 8, 2026 · 00:04 UTC
import {
App,
applyDocumentTheme,
applyHostFonts,
applyHostStyleVariables,
} from "@modelcontextprotocol/ext-apps/app-with-deps";
import { AppLogger } from "@oai/first-party-plugin-bridge/logging";
type ToolArguments = Record<string, unknown>;
export type HostContext = ReturnType<App["getHostContext"]>;
type HostContextListener = (context: NonNullable<HostContext>) => void;
export interface McpToolCaller {
call<T>(name: string, arguments_: ToolArguments): Promise<T>;
}
export interface NgsWorkbenchClient extends McpToolCaller {
readonly logger: Pick<AppLogger, "error">;
connect(): Promise<void>;
getHostContext(): HostContext;
onHostContextChanged(listener: HostContextListener): () => void;
openLink(url: string): Promise<{ isError?: boolean }>;
}
export class NgsMcpClient implements NgsWorkbenchClient {
private readonly app: App;
readonly logger: Pick<AppLogger, "error" | "warn">;
constructor(
app = new App(
{ name: "NGS Analysis Workbench", version: "0.1.0" },
{ availableDisplayModes: ["inline", "fullscreen"] },
{ autoResize: true },
),
logger: Pick<AppLogger, "error" | "warn"> = new AppLogger(app, "workbench"),
) {
this.app = app;
this.logger = logger;
}
async connect() {
try {
await this.app.connect();
} catch (error) {
this.logger.error("Failed to connect NGS workbench app", {
errorName: error instanceof Error ? error.name : typeof error,
});
throw error;
}
}
getHostContext() {
return this.app.getHostContext();
}
onHostContextChanged(listener: HostContextListener) {
const handler = (context: NonNullable<HostContext>) => listener(context);
this.app.addEventListener("hostcontextchanged", handler);
return () => this.app.removeEventListener("hostcontextchanged", handler);
}
async call<T>(name: string, arguments_: ToolArguments): Promise<T> {
let result: Awaited<ReturnType<App["callServerTool"]>>;
try {
result = await this.app.callServerTool({ name, arguments: arguments_ });
} catch (error) {
this.logger.error("MCP tool request failed", {
errorName: error instanceof Error ? error.name : typeof error,
tool: name,
});
throw error;
}
if (isRecord(result) && result.isError === true) {
const error = new Error(toolErrorMessage(result));
this.logger.warn("MCP tool returned an error", { tool: name });
throw error;
}
if (!isRecord(result) || !isRecord(result.structuredContent)) {
const error = new Error(`${name} did not return structured content.`);
this.logger.error("MCP tool returned invalid structured content", {
tool: name,
});
throw error;
}
return result.structuredContent as T;
}
openLink(url: string) {
return this.app.openLink({ url });
}
}
export function applyHostContext(context: HostContext) {
if (!context) return;
if (context.theme) applyDocumentTheme(context.theme);
if (context.styles?.variables) applyHostStyleVariables(context.styles.variables);
if (context.styles?.css?.fonts) applyHostFonts(context.styles.css.fonts);
}
function toolErrorMessage(result: Record<string, unknown>) {
const content = result.content;
if (!Array.isArray(content)) return "MCP tool call failed.";
const text = content.find(
(item) => isRecord(item) && item.type === "text" && typeof item.text === "string",
);
return isRecord(text) && typeof text.text === "string" ? text.text : "MCP tool call failed.";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
SHA-256: aeb57dd10aac56b054d8f582d51d37e7d1b255f8a106cb62c9470d5df8d9ff5e