In the previous Chapter 1: Tool Metadata, we gave our tool an identity (a name and a description). The AI now knows the tool exists and generally what it does.
However, a description is just text. It doesn't force the AI to speak our language.
Imagine you run a pizza shop. If you just tell customers "Order here," they might say:
Your kitchen (the code) will crash because it expects a specific format, like: { size: "large", topping: "pepperoni" }.
To solve this, we need a Data Schema.
A Schema acts like a strict form or a Bouncer. It stands at the door of your tool and says: "You cannot come in unless your data looks exactly like this."
In this project, we use a library called Zod. Zod allows us to define blueprints for our data. If the data doesn't match the blueprint, Zod throws an error before our main code even runs.
We need to define two schemas:
We will be writing this logic in ListMcpResourcesTool.ts.
Our use case is simple: We want the AI to list resources. Optionally, the AI might want to filter resources by a specific server.
Here is how we define that rule:
import { z } from 'zod/v4'
import { lazySchema } from '../../utils/lazySchema.js'
const inputSchema = lazySchema(() =>
z.object({
server: z
.string()
.optional()
.describe('Optional server name to filter resources by'),
}),
)
Explanation:
z.object({...}): The input must be a JSON object.server: This is the name of the allowed parameter.z.string(): The value MUST be text (not a number)..optional(): The AI doesn't have to provide this. It can leave it blank..describe(...): This text is sent to the AI to help it understand the parameter's purpose.
Note: We wrap this in lazySchema. This is a performance trick that says, "Don't build this object until the tool is actually used," which makes the app start faster.
When our tool finishes running, it returns a list of resources. We need to guarantee that this list follows a strict shape so the application doesn't crash when trying to display it.
const outputSchema = lazySchema(() =>
z.array(
z.object({
uri: z.string().describe('Resource URI'),
name: z.string().describe('Resource name'),
server: z.string().describe('Server that provides this resource'),
// We also track mimeType and description (omitted for brevity)
}),
),
)
Explanation:
z.array(...): We are returning a list, not a single item.uri, name, server: Every single item in the list MUST have these fields.name, this schema will catch the bug immediately.One of the best features of Zod is that it works with TypeScript. We don't have to write separate TypeScript interfaces. We can extract them directly from the schema.
// Create a TypeScript type based on the Input definition
type InputSchema = ReturnType<typeof inputSchema>
// Create a TypeScript type based on the Output definition
type OutputSchema = ReturnType<typeof outputSchema>
// This helps us use the output type elsewhere in the app
export type Output = z.infer<OutputSchema>
Explanation:
z.infer<...>: This command looks at the Zod code we wrote above and automatically generates the TypeScript type (e.g., { uri: string, name: string }).What happens when the AI tries to use our tool? Let's visualize the process.
The schema acts as a filter before and after the tool logic runs.
In the actual file ListMcpResourcesTool.ts, these schemas are properties of the tool object.
When we eventually build the tool definition, we attach these schemas so the system can access them.
// ... inside the tool definition object ...
get inputSchema(): InputSchema {
return inputSchema() // Calls the lazy loader
},
get outputSchema(): OutputSchema {
return outputSchema() // Calls the lazy loader
},
Explanation:
get inputSchema()). This allows the system to ask for the schema only when it needs to perform validation.In this chapter, we established the Data Schemas.
server must be a string.Now that we have the Metadata (Chapter 1) and the Schemas (Chapter 2), we have all the building blocks needed to construct the actual tool logic.
In the next chapter, we will combine these into the full Tool Definition and write the code that actually fetches the data.
Generated by Code IQ