Type Safety
Problem: Your server defines tool schemas, but your view 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
Section titled “The Challenge”Without type inference, you duplicate types:
server.registerTool( { name: "search", inputSchema: { query: z.string(), limit: z.number() }, view: { component: "search" }, }, async ({ query, limit }) => { // ... },);
// src/views/search.tsxtype SearchInput = { query: string; limit: number }; // Duplicated!type SearchOutput = { results: Result[] }; // Duplicated!
const { callTool } = useCallTool<SearchInput, SearchOutput>("search");If the server schema changes, the view types are now wrong.
The Solution: generateHelpers
Section titled “The Solution: generateHelpers”Export your server’s type and use generateHelpers:
Step 1: Export the server type
import { McpServer } from "@usefractal/frac/server";import { z } from "zod";
const server = new McpServer({ name: "my-app", version: "1.0" }, {}) .registerTool( { name: "search-hotels", inputSchema: { city: z.string(), checkIn: z.string(), }, outputSchema: { hotels: z.array(z.object({ id: z.string(), name: z.string() })), }, view: { component: "search-hotels" }, }, async ({ city, checkIn }) => { const hotels = await searchHotels(city, checkIn); return { structuredContent: { hotels } }; }, ) .registerTool( { name: "hotel-details", inputSchema: { hotelId: z.string() }, view: { component: "hotel-details" }, }, async ({ hotelId }) => { const hotel = await getHotel(hotelId); return { structuredContent: hotel }; }, );
// Export the typeexport type AppType = typeof server;Step 2: Generate typed hooks
import type { AppType } from "./server";import { generateHelpers } from "@usefractal/frac/web";
export const { useCallTool, useToolInfo } = generateHelpers<AppType>();Step 3: Use typed hooks
import { useCallTool } from "../helpers";
export default function SearchHotels() { 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
Section titled “Method Chaining Requirement”// Works — types accumulate through the chainconst server = new McpServer({ name: "app", version: "1.0" }, {}) .registerTool({ name: "a", view: { component: "a" } }, async () => ({ structuredContent: {} })) .registerTool({ name: "b", view: { component: "b" } }, async () => ({ structuredContent: {} }));
// Doesn't work — typeof server = McpServer<{}> (empty!)const server = new McpServer({ name: "app", version: "1.0" }, {});server.registerTool({ name: "a", view: { component: "a" } }, async () => ({ structuredContent: {} }));server.registerTool({ name: "b", view: { component: "b" } }, async () => ({ structuredContent: {} }));How It Works
Section titled “How It Works”The $types property pattern enables cross-package type inference:
// McpServer internally tracks tool typesclass McpServer<ToolRegistry = {}> { $types!: McpServerTypes<ToolRegistry>;
registerTool<Name, Input, Output>( config: { name: Name; inputSchema?: Input; view?: ViewConfig; /* ... */ }, ... ): 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
Section titled “Type Utilities”frac exports utilities for extracting types:
import type { InferTools, ToolNames, ToolInput, ToolOutput,} from "@usefractal/frac/server";import type { AppType } from "./server";
// Get all tool names as a uniontype MyToolNames = ToolNames<AppType>;// "search-hotels" | "hotel-details"
// Get input type for a specific tooltype SearchInput = ToolInput<AppType, "search-hotels">;// { city: string; checkIn: string }
// Get output type for a specific tooltype SearchOutput = ToolOutput<AppType, "search-hotels">;// { hotels: { id: string; name: string }[] }Zod Schema Connection
Section titled “Zod Schema Connection”The magic comes from Zod schemas. When you define:
inputSchema: { city: z.string(), limit: z.number().optional(),}frac 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
Section titled “Troubleshooting””Property does not exist on type”
Section titled “”Property does not exist on type””Make sure you’re importing from your generated helpers file, not directly from @usefractal/frac/web:
// Wrongimport { useCallTool } from "@usefractal/frac/web";
// Rightimport { useCallTool } from "../helpers";No autocomplete for tool names
Section titled “No autocomplete for tool names”Check that:
- Your server exports
type AppType = typeof server - Your
helpers.tsimports this type correctly - You’re using method chaining
Related
Section titled “Related”- generateHelpers API - Full API documentation
- Type Utilities - Type extraction helpers