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";
Check that:
- Your server exports
type AppType = typeof server
- Your
skybridge.ts imports this type correctly
- You’re using method chaining