Welcome to the MCPTool project tutorial! In this first chapter, we are going to build the foundation of our tool system: the Universal Tool Adapter.
Imagine you are building an AI assistant. You want it to use a calculator, check the weather, and search GitHub.
Without a standard system, you might write code like this:
If you want to add a fourth tool, you have to write new code again. This is hard to maintain.
Instead of hardcoding every tool, we create a Universal Tool Adapter.
Think of this like a universal international travel adapter for wall sockets.
MCPTool.No matter what device (tool) you have, you plug it into the Adapter, and the Adapter plugs into your App. Your App doesn't need to know the details of the device; it just talks to the Adapter.
Before we look at the code, let's look at the flow. When your application wants to use a tool, it doesn't call the tool directly. It calls our Adapter.
Here is a simple diagram of this interaction:
Let's look at MCPTool.ts. This file defines the template for our adapter.
First, we need to tell our code what kind of inputs to accept. Since this is a Universal adapter, we need to accept anything.
We use a library called zod for validation.
// File: MCPTool.ts
import { z } from 'zod/v4'
import { lazySchema } from '../../utils/lazySchema.js'
// Allow any input object. 'passthrough' means "don't filter out unknown fields"
export const inputSchema = lazySchema(() => z.object({}).passthrough())
type InputSchema = ReturnType<typeof inputSchema>
Explanation:
z.object({}): Expects an object (like a JSON)..passthrough(): This is the magic. It tells the adapter, "If the tool requires specific arguments (like city for weather), just let them pass through. Don't block them."Next, we define what the output looks like. Even though inputs vary, we always want the output to be a consistent string format for our App to read.
// File: MCPTool.ts
export const outputSchema = lazySchema(() =>
z.string().describe('MCP tool execution result'),
)
type OutputSchema = ReturnType<typeof outputSchema>
Explanation:
z.string(): We expect the result to be text (e.g., "The weather is sunny" or "4").Now we combine everything into the tool definition. This creates the "plug."
Note: In the actual codebase, properties like name, description, and call are often overridden later when we connect to a specific real-world tool. This file acts as the generic template.
// File: MCPTool.ts
import { buildTool } from '../../Tool.js'
export const MCPTool = buildTool({
isMcp: true,
name: 'mcp', // A default name, usually replaced later
// Connect the schemas we defined above
get inputSchema(): InputSchema {
return inputSchema()
},
// ... (continued below)
Finally, we define the default behavior for calling the tool and checking if it's safe.
// File: MCPTool.ts (continued)
// This is a placeholder. The real logic is injected when the client starts.
async call() {
return { data: '' }
},
// Default permission check
async checkPermissions(): Promise<PermissionResult> {
return {
behavior: 'passthrough',
message: 'MCPTool requires permission.',
}
},
})
Explanation:
call(): Currently returns empty data. Why? Because this is just the adapter shell. Later, when we connect a "Weather" server, this function is replaced with code that actually calls the Weather API.checkPermissions(): Ensures the user approves the action before the tool runs.In this chapter, we created the Universal Tool Adapter.
MCPTool, a wrapper that accepts any input (passthrough) and standardizes the output.But waitβif this adapter can handle any tool, how does the AI know which tool to pick? Or how to interpret the user's messy request?
We will solve that in the next chapter.
Next Chapter: Interaction Classifier
Generated by Code IQ