Skip to main content
registerTool adds an action the model can call to your server, optionally rendering its result through a view.

Example

search-products searches the store catalog for the signed-in user and renders the matches in the carousel view.
server.ts
import { McpServer } from "skybridge/server";
import { z } from "zod";

const server = new McpServer({ name: "shop", version: "1.0" }).registerTool(
  {
    name: "search-products",
    title: "Search products",
    description: "Search the store catalog by keyword and optional price ceiling.",
    inputSchema: {
      query: z.string().describe("What the shopper is looking for"),
    },
    outputSchema: {
      productIds: z.array(z.string()).describe("The IDs of products matching the query."),
    },
    annotations: { readOnlyHint: true },
    securitySchemes: [{ type: "oauth2", scopes: ["catalog.read"] }],
    view: {
      component: "carousel",
      csp: { connectDomains: ["https://api.myshop.com"] },
    },
  },
  async ({ query, maxPrice }, { authInfo }) => {
    const userId = authInfo.extra?.sub as string;
    const products = await search(query, { maxPrice, userId });
    return {
      content: `Found ${products.length} products for "${query}".`,
      structuredContent: { products },
    };
  },
);

Signature

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

Config

The first argument declares the tool.
type ToolConfig = {
  name: string;
  title?: string; // human-readable label
  description?: string; // model-facing: decides when the model calls the tool
  inputSchema?: ZodRawShape; // Zod shape; validates the call and types the handler input
  outputSchema?: ZodRawShape; // Zod shape; types structuredContent
  annotations?: ToolAnnotations; // standard MCP hints
  view?: ViewConfig; // bind a view to render the result
  securitySchemes?: SecurityScheme[]; // declare per-tool auth
  _meta?: ToolMeta; // extra metadata
};

name, title, description

All three are the tool’s prompt surface: the model reads them to decide when to call it, so write each for the model. name is also the identifier the call uses, and title a short display name.

inputSchema, outputSchema

Zod raw shapes. inputSchema validates the call arguments and types the handler’s input. Add .describe() to a field to tell the model what it’s for. outputSchema tells the model the shape to expect back, it does not type the returned value.

annotations

Standard MCP hints describing what the tool does, so the host can label it and order calls. They are hints: the host may surface or ignore them, and never gates a call on them.
type ToolAnnotations = {
  title?: string;
  readOnlyHint?: boolean;
  destructiveHint?: boolean;
  idempotentHint?: boolean;
  openWorldHint?: boolean;
};
HintMeaningDefault
titleHuman-readable label; the host gives it precedence over name.none
readOnlyHintThe tool does not modify its environment.false
destructiveHintThe tool may perform destructive updates, not only additive ones. Meaningful only when readOnlyHint is false.true
idempotentHintRepeating the call with the same arguments has no further effect. Meaningful only when readOnlyHint is false.false
openWorldHintThe tool interacts with an open world of external entities, not a closed domain.true

view

Bind the tool to a React view to render its result instead of plain text. Each view backs exactly one tool.
type ViewConfig = {
  component: ViewName;
  description?: string;
  hosts?: Array<"apps-sdk" | "mcp-app">;
  prefersBorder?: boolean;
  domain?: string;
  csp?: ViewCsp;
  _meta?: Record<string, unknown>;
};
KeyPurpose
componentThe view’s file name, type-checked against your views.
descriptionLabel the host may show during view discovery.
hostsRestrict where the view renders; defaults to all.
prefersBorderApps SDK only: request a visible border around the view.
domainApps SDK only: override the served domain (advanced).
cspPer-view CSP overrides; see below.
_metaFree-form metadata forwarded on the view resource.

csp

A view runs in a sandboxed iframe; your server’s domain is allowlisted automatically. Add external origins per directive:
type ViewCsp = {
  resourceDomains?: string[];
  connectDomains?: string[];
  frameDomains?: string[];
  redirectDomains?: string[];
  baseUriDomains?: string[];
};
DirectivePurpose
resourceDomainsStatic assets: images, fonts, scripts, styles.
connectDomainsFetch / XHR targets.
frameDomainsIframe embed origins (opts into stricter app review).
redirectDomainsopenExternal targets that skip the safe-link modal.
baseUriDomains<base href> origins (MCP Apps only).

securitySchemes

Declare which auth a tool supports so clients can label it public vs. requires-sign-in before invocation, and request the right scopes during sign-in.
type SecurityScheme =
  | { type: "noauth" }
  | { type: "oauth2"; scopes?: string[] };
  • Across the array, match any. Each entry is an alternative.
  • Within an oauth2 entry’s scopes, match all. The token must carry every listed scope.
  • noauth and oauth2 together means “works anonymously, but auth unlocks more.”
securitySchemes is client-facing metadata, not self-enforcing. Pair it with an auth middleware (requireBearerAuth or optionalBearerAuth) to authenticate requests, and enforce the tool’s own requirements in the handler from extra.authInfo.

_meta

Metadata on the tool. Skybridge recognizes the keys below; any other key you set is forwarded on the tool’s _meta untouched.
type ToolMeta = {
  ui?: { visibility?: Array<"model" | "app"> };
  "openai/toolInvocation/invoking"?: string;
  "openai/toolInvocation/invoked"?: string;
  "openai/fileParams"?: string[];
  [key: string]: unknown;
};
KeyPurpose
ui.visibilityExpose the tool to the model, the app, or both.
openai/toolInvocation/invoking, invokedStatus text shown while the tool runs and after it completes.
openai/fileParamsTop-level input fields that carry file references; see FileRef.
openai/widgetAccessibleDeprecated, use ui.visibility.
The openai/* keys are read only on ChatGPT. openai/widgetAccessibleis deprecated, prefer ui.visibility instead.

Handler

Runs when the tool is called. It receives the validated input (typed from inputSchema) and the request context extra:
type ToolHandler = (input: Input, extra: RequestHandlerExtra) => Promise<{
  content?: string | ContentBlock | ContentBlock[];
  structuredContent?: Record<string, unknown>;
  isError?: boolean;
  _meta?: Record<string, unknown>;
}>;

extra

The request context.
FieldPurpose
authInfoValidated token from an auth middleware: clientId, scopes, extra.
requestInfoThe HTTP request: headers and URL.
signalAn AbortSignal, aborted when the call is cancelled.
_metaClient hints (below).

Return

FieldPurpose
contentText for the model, shown in the conversation. A string, a ContentBlock, or an array.
structuredContentTyped data the model and the view read (via useToolInfo).
isErrorMarks the call as failed.
_metaView-only response metadata, hidden from the model.
For a view tool, Skybridge adds a viewUUID to _meta, a reserved key used for state persistence.

Client hints

ChatGPT attaches conversation context to extra._meta. They are hints: tolerate their absence and never use them for authorization.
type ClientHintsMeta = {
  "openai/locale"?: string;
  "openai/userAgent"?: string;
  "openai/userLocation"?: {
    city?: string;
    region?: string;
    country?: string;
    timezone?: string;
    longitude?: number;
    latitude?: number;
  };
  "openai/subject"?: string;
  "openai/session"?: string;
  "openai/organization"?: string;
  "openai/widgetSessionId"?: string;
};
KeyPurpose
openai/localeRequested locale, BCP-47, e.g. en-US.
openai/userAgentBrowser user-agent of the ChatGPT client.
openai/userLocationCoarse location: city, region, country, timezone, latitude, longitude (each optional).
openai/subjectAnonymized user id.
openai/sessionAnonymized conversation id, stable within a session.
openai/organizationAnonymized organization id, when the user account is in an organization.
openai/widgetSessionIdStable id for the mounted view instance.
These hints are ChatGPT only.

Content helpers

skybridge/server exports helpers that build ContentBlocks for the handler’s content:
import { audio, embeddedResource, image, resourceLink, text } from "skybridge/server";

return {
  content: [text("Here's your chart:"), image(pngBuffer, "image/png")],
  structuredContent: { /* ... */ },
};
HelperBlock
text(value, annotations?)TextContent
image(data, mimeType, annotations?)ImageContent (base64)
audio(data, mimeType, annotations?)AudioContent (base64)
embeddedResource(resource, annotations?)EmbeddedResource
resourceLink(link, annotations?)ResourceLink
Returning plain ContentBlock objects works too; the helpers are optional.

McpServer

The server you register tools on

useToolInfo

Read the tool’s result in the view

FileRef

Pass files in and out of a tool