Skip to content

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.

Without type inference, you duplicate types:

src/server.ts
server.registerTool(
{
name: "search",
inputSchema: { query: z.string(), limit: z.number() },
view: { component: "search" },
},
async ({ query, limit }) => {
// ...
},
);
// src/views/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 view types are now wrong.

Export your server’s type and use generateHelpers:

Step 1: Export the server type

src/server.ts
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 type
export type AppType = typeof server;

Step 2: Generate typed hooks

src/helpers.ts
import type { AppType } from "./server";
import { generateHelpers } from "@usefractal/frac/web";
export const { useCallTool, useToolInfo } = generateHelpers<AppType>();

Step 3: Use typed hooks

src/views/search-hotels.tsx
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
}
}
// Works — types accumulate through the chain
const 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: {} }));

The $types property pattern enables cross-package type inference:

// McpServer internally tracks tool types
class 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]
},
};
}

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 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 }[] }

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")[];
};
}

Make sure you’re importing from your generated helpers file, not directly from @usefractal/frac/web:

// Wrong
import { useCallTool } from "@usefractal/frac/web";
// Right
import { useCallTool } from "../helpers";

Check that:

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