← Files NGS Analysis WorkbenchARCHIVED FILE

src/mcp/client.ts

3.61 KB · Oct 8, 2026 · 00:04 UTC

↓ Download file

See the change to this file →

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