๐Ÿ“ tools/ListMcpResourcesTool/ ยท 02_data_schemas.md

Chapter 2: Data Schemas

๐Ÿ“„ tools/ListMcpResourcesTool/02_data_schemas.md

Chapter 2: Data Schemas

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.

The Problem: The "Pizza Order" Chaos

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."

The Solution: Zod

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:

  1. Input Schema: What parameters can the AI send us?
  2. Output Schema: What does the data look like that we send back?

We will be writing this logic in ListMcpResourcesTool.ts.

1. The Input Schema

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:

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.

2. The Output Schema

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:

3. Type Inference (TypeScript Magic)

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:

Under the Hood: The "Bouncer" Workflow

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.

sequenceDiagram participant AI as AI Model participant Bouncer as Zod Schema participant Tool as Tool Logic Note over AI: User asks: "Show resources from 'myserver'" AI->>Bouncer: Input: { server: 12345 } Note right of AI: Mistake! Server name should be string. Bouncer--xAI: ERROR: Expected string, received number. AI->>Bouncer: Input: { server: "myserver" } Note right of AI: Correct format. Bouncer->>Tool: Pass data through Note over Tool: Tool fetches data... Tool->>Bouncer: Output: [{ name: "File A", server: "myserver" }] Bouncer->>AI: Data matches Output Schema. Delivered.

Internal Implementation

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:

Summary

In this chapter, we established the Data Schemas.

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.

Next Chapter: Tool Definition


Generated by Code IQ