Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.skybridge.tech/llms.txt

Use this file to discover all available pages before exploring further.

Register an MCP tool on the server. Provide a view to bind the tool to a React view; omit view for a plain tool with no UI.

Signature

server.registerTool(
  config: ToolConfig,
  handler: ToolHandler,
): McpServer

Config

type ToolConfig = {
  name: string;              // Tool name, exposed to the model
  title?: string;            // Human-readable title
  description?: string;      // Shown to the model
  inputSchema?: ZodRawShape; // Input validation (Zod)
  outputSchema?: ZodRawShape;// Output type hints (Zod)
  annotations?: ToolAnnotations;
  view?: ViewConfig;         // ← present = view-tool, absent = plain tool
  _meta?: ToolMeta;
};

type ToolMeta = {
  // Enable view-initiated tool calls (see useCallTool)
  "openai/widgetAccessible"?: boolean;
  // Custom status messages
  "openai/toolInvocation/invoking"?: string; // While running
  "openai/toolInvocation/invoked"?: string;  // After completion
  [key: string]: unknown;
};

view

Binds the tool to a React view.
type ViewConfig = {
  component: ViewName;                   // File name in src/views/ (typed — see below)
  description?: string;                    // Shown during view discovery
  hosts?: ("apps-sdk" | "mcp-app")[];      // Defaults to both
  prefersBorder?: boolean;                 // Removes the default iframe border when false
  domain?: string;                         // Custom view domain
  csp?: {
    resourceDomains?: string[];   // Static assets (images, fonts, scripts)
    connectDomains?: string[];    // Fetch / XHR targets
    frameDomains?: string[];      // Iframe embed origins
    redirectDomains?: string[];   // Allowed external redirects (skips safe-link modal)
    baseUriDomains?: string[];    // `<base href>` origins (mcp-apps only)
  };
  _meta?: Record<string, unknown>;
};
See CSP & Domains below.

Handler

type ToolHandler = (
  input: Input,
  extra: RequestHandlerExtra,
) => Promise<{
  content?: string | ContentBlock | ContentBlock[]; // Text(s) for the model
  structuredContent?: StructuredData;               // Data for the view
  _meta?: Record<string, unknown>;                  // Response metadata
}>;
content can be a plain string, a single ContentBlock, or an array of ContentBlocks. See the different content helpers that Skybridge provides. The extra parameter exposes:
  • extra.authInfo — populated by auth middleware (e.g. authInfo.clientId, authInfo.extra.email)
  • extra.requestInfo — HTTP request details (headers, URL)
  • extra.signal — abort signal for cancellation

Response Fields

Three fields, three audiences:
FieldPurposeConsumed by
contentText descriptionThe host (shown in conversation)
structuredContentTyped dataHost and view (useToolInfo, useCallTool)
_metaResponse metadataView only (hidden from the model)
See Data Flow: Response Fields.
viewUUID: Skybridge automatically injects a unique viewUUID into _meta for every tool with view call. This enables useViewState and createStore to persist state across re-renders in MCP Apps via localStorage. Any _meta you return is preserved alongside the injected viewUUID.

Examples

Tool with a view

File src/views/greeting.tsx exports the React component. view.component matches its file name.
import { McpServer } from "skybridge/server";
import { z } from "zod";

const server = new McpServer({ name: "my-app", version: "0.0.1" }, {})
  .registerTool(
    {
      name: "greet",
      description: "Greet someone by name.",
      inputSchema: { name: z.string() },
      view: { component: "greeting", description: "A greeting card" },
    },
    async ({ name }) => {
      return {
        content: `Greeting ${name}`,
        structuredContent: { message: `Hello, ${name}!` },
      };
    },
  );

Tool without view

server.registerTool(
  {
    name: "calculate",
    description: "Evaluate an arithmetic expression.",
    inputSchema: { expression: z.string() },
  },
  async ({ expression }) => {
    const value = evaluate(expression);
    return { content: `Result: ${value}` };
  },
);

With external API and CSP

server.registerTool(
  {
    name: "weather",
    description: "Show weather for a location.",
    inputSchema: {
      city: z.string(),
    },
    outputSchema: {
      temperature: z.number(),
      conditions: z.string(),
      humidity: z.number(),
    },
    view: {
      component: "weather",
    },
  },
  async ({ city }) => {
    const weather = await fetchWeather(city);
    return {
      content: `Current weather in ${city}: ${weather.temperature}°, ${weather.conditions}`,
      structuredContent: weather,
    };
  },
);

With auth context

Pull authentication info from extra (requires auth middleware via .use()):
server.registerTool(
  {
    name: "user-profile",
    description: "Show user profile.",
    inputSchema: { userId: z.string() },
    view: { component: "user-profile" },
  },
  async ({ userId }, extra) => {
    const email = extra.authInfo?.extra?.email;
    const user = await getUser(userId);
    if (!user) {
      return {
        content: "Error: User not found",
        isError: true,
      };
    }

    return {
      content: `Showing profile for ${user.name}`,
      structuredContent: { ...user, viewedBy: email },
    };
  },
);

Content helpers

skybridge/server exports helpers for building ContentBlocks:
import { audio, embeddedResource, image, resourceLink, text } from "skybridge/server";

return {
  content: [
    text("Here's your chart:"),
    image(pngBuffer, "image/png"),
  ],
  structuredContent: { /* … */ },
};
HelperBlock type
text(value, annotations?)TextContent
image(data, mimeType, annotations?)ImageContent (base64-encoded)
audio(data, mimeType, annotations?)AudioContent (base64-encoded)
embeddedResource(resource, annotations?)EmbeddedResource
resourceLink(link, annotations?)ResourceLink
You can still return plain ContentBlock objects — helpers are opt-in.

Input Schema

Define expected inputs with Zod:
inputSchema: {
  query: z.string().describe("Search query"),
  limit: z.number().optional().default(10),
  sortBy: z.enum(["price", "rating", "distance"]),
  filters: z.object({
    minPrice: z.number().optional(),
    maxPrice: z.number().optional(),
  }).optional(),
  amenities: z.array(z.string()).optional(),
}

Output Schema

Optional type hints:
outputSchema: {
  results: z.array(z.object({
    id: z.string(),
    name: z.string(),
    price: z.number(),
  })),
  totalCount: z.number(),
}

CSP & Domains

Views run in a sandboxed iframe with strict Content Security Policy. If your view needs to:
  • Fetch data from external APIs → add to connectDomains
  • Load images, fonts, or scripts from CDNs → add to resourceDomains
  • Embed external iframes → add to frameDomains
  • Redirect to external sites → add to redirectDomains
Skybridge automatically includes your server’s domain in the CSP. You only need to configure additional external domains.

Configuration

server.registerTool(
  {
    name: "flight-search",
    description: "Search for flights",
    inputSchema: { /* … */ },
    view: {
      component: "flight-search",
      prefersBorder: false,
      csp: {
        connectDomains: ["https://api.flights.com"],
        resourceDomains: ["https://cdn.flights.com", "https://fonts.googleapis.com"],
        frameDomains: ["https://maps.google.com"],
        redirectDomains: ["https://booking.flights.com"],
      },
    },
  },
  async (input) => { /* … */ },
);

CSP Fields

FieldPurposeExample
connectDomainsFetch/XHR targets["https://api.example.com"]
resourceDomainsStatic assets (images, fonts, scripts, styles)["https://cdn.example.com"]
frameDomainsIframe embed origins["https://youtube.com"]
redirectDomainsExternal redirect destinations["https://checkout.example.com"]
baseUriDomains<base href> origins (mcp-apps only)["https://app.example.com"]