Skip to main content
Problem: Your server defines tool schemas, but your widget has no idea what types to expect. You end up duplicating type definitions or using any. Solution: generateHelpers creates typed hooks from your server type, giving you autocomplete and type checking across the stack.

The Challenge

Without type inference, you duplicate types:
// server/src/index.ts
server.registerWidget("search", {}, {
  inputSchema: { query: z.string(), limit: z.number() },
}, async ({ query, limit }) => {
  // ...
});

// web/src/widgets/search.tsx
type SearchInput = { query: string; limit: number }; // Duplicated!
type SearchOutput = { results: Result[] }; // Duplicated!

const { callTool } = useCallTool<SearchInput, SearchOutput>("search");
If the server schema changes, the widget types are now wrong.

The Solution: generateHelpers

Export your server’s type and use generateHelpers: Step 1: Export the server type
// server/src/index.ts
import { McpServer } from "skybridge/server";
import { z } from "zod";

const server = new McpServer({ name: "my-app", version: "1.0" }, {})
  .registerWidget("search-hotels", {}, {
    inputSchema: {
      city: z.string(),
      checkIn: z.string(),
    },
    outputSchema: {
      hotels: z.array(z.object({ id: z.string(), name: z.string() })),
    },
  }, async ({ city, checkIn }) => {
    const hotels = await searchHotels(city, checkIn);
    return { structuredContent: { hotels } };
  })
  .registerWidget("hotel-details", {}, {
    inputSchema: { hotelId: z.string() },
  }, async ({ hotelId }) => {
    const hotel = await getHotel(hotelId);
    return { structuredContent: hotel };
  });

// Export the type
export type AppType = typeof server;
Step 2: Generate typed hooks
// web/src/helpers.ts
import type { AppType } from "../../server/src/index";
import { generateHelpers } from "skybridge/web";

export const { useCallTool, useToolInfo } = generateHelpers<AppType>();
Step 3: Use typed hooks
// web/src/widgets/search.tsx
import { useCallTool } from "../skybridge";

export function SearchWidget() {
  const { callTool, data } = useCallTool("search-hotels");
  //                                       ^ autocomplete shows available tools

  callTool({ city: "Paris", checkIn: "2025-12-15" });
  //         ^ autocomplete for input fields

  if (data) {
    data.structuredContent.hotels.map(hotel => hotel.name);
    //                              ^ fully typed
  }
}

Method Chaining Requirement

Required for type inferenceYou must use method chaining when registering tools. TypeScript captures types at assignment — without chaining, typeof server sees an empty registry.
// Works — types accumulate through the chain
const server = new McpServer({ name: "app", version: "1.0" }, {})
  .registerWidget("a", {}, {}, async () => ({ structuredContent: {} }))
  .registerWidget("b", {}, {}, async () => ({ structuredContent: {} }));

// Doesn't work — typeof server = McpServer<{}> (empty!)
const server = new McpServer({ name: "app", version: "1.0" }, {});
server.registerWidget("a", {}, {}, async () => ({ structuredContent: {} }));
server.registerWidget("b", {}, {}, async () => ({ structuredContent: {} }));

How It Works

The $types property pattern enables cross-package type inference:
// McpServer internally tracks tool types
class McpServer<ToolRegistry = {}> {
  $types!: McpServerTypes<ToolRegistry>;

  registerWidget<Name, Input, Output>(
    name: Name,
    ...
  ): McpServer<ToolRegistry & { [K in Name]: ToolDef<Input, Output> }> {
    // Returns a new type with the tool added
  }
}
generateHelpers extracts these types:
function generateHelpers<ServerType>() {
  type Tools = InferTools<ServerType>;

  return {
    useCallTool: <ToolName extends keyof Tools>(name: ToolName) => {
      // Input and output types are inferred from Tools[ToolName]
    },
  };
}

Type Utilities

Skybridge exports utilities for extracting types:
import type {
  InferTools,
  ToolNames,
  ToolInput,
  ToolOutput,
} from "skybridge/web";
import type { AppType } from "../../server/src/index";

// Get all tool names as a union
type MyToolNames = ToolNames<AppType>;
// "search-hotels" | "hotel-details"

// Get input type for a specific tool
type SearchInput = ToolInput<AppType, "search-hotels">;
// { city: string; checkIn: string }

// Get output type for a specific tool
type SearchOutput = ToolOutput<AppType, "search-hotels">;
// { hotels: { id: string; name: string }[] }

Zod Schema Connection

The magic comes from Zod schemas. When you define:
inputSchema: {
  city: z.string(),
  limit: z.number().optional(),
}
Skybridge infers:
type Input = {
  city: string;
  limit?: number;
}
Complex Zod types work too:
inputSchema: {
  filters: z.object({
    minPrice: z.number(),
    maxPrice: z.number(),
    amenities: z.array(z.enum(["wifi", "pool", "gym"])),
  }).optional(),
}
Becomes:
type Input = {
  filters?: {
    minPrice: number;
    maxPrice: number;
    amenities: ("wifi" | "pool" | "gym")[];
  };
}

Troubleshooting

”Property does not exist on type”

Make sure you’re importing from your generated helpers file, not directly from skybridge/web:
// Wrong
import { useCallTool } from "skybridge/web";

// Right
import { useCallTool } from "../skybridge";

No autocomplete for tool names

Check that:
  1. Your server exports type AppType = typeof server
  2. Your skybridge.ts imports this type correctly
  3. You’re using method chaining