Welcome to the first chapter of our tutorial! Here, we will lay the foundation for creating a powerful tool called ReadMcpResourceTool.
Imagine you have a library full of books (resources), but the librarian (the AI) doesn't know they exist or how to open them. You need a way to formally introduce a specific abilityβlike "Read Book"βto the AI.
In this project, our "books" are resources on an MCP (Model Context Protocol) server. The Use Case we are solving is simple:
A user wants the AI to read the contents of a specific file or resource (identified by a URI) from a connected server.
To make this happen, we need to create a Tool Definition. Think of this as filling out a registration card or creating an "ID Badge" for the tool. This badge tells the system:
We use a helper function called buildTool to create this definition. Let's break down the parts of our ID Badge.
The system needs to know how to find your tool. We provide a unique name and a searchHint to help the system's "router" pick the right tool for the job.
These are like safety labels on power tools.
call FunctionThis is the engine. When the AI decides to use the tool, this function runs. It takes inputs (like a server name and a URI) and returns the result.
Let's look at how we build the ReadMcpResourceTool. We will write this in a file typically named ReadMcpResourceTool.ts.
First, we start the buildTool function and give it a name.
export const ReadMcpResourceTool = buildTool({
name: 'ReadMcpResourceTool',
// A short hint for the system's search algorithm
searchHint: 'read a specific MCP resource by URI',
// A detailed description for the AI to understand purpose
async description() { return DESCRIPTION },
// Specific instructions on how the AI should use it
async prompt() { return PROMPT },
// ...
Explanation: We export the tool so other parts of the app can load it. The searchHint is crucial for performanceβit helps the system quickly guess if this tool is relevant before doing a deep analysis.
Next, we define the behavioral flags.
// ... inside buildTool object
isConcurrencySafe() {
return true
},
isReadOnly() {
return true
},
// If the result is huge, stop at 100k characters
maxResultSizeChars: 100_000,
// ...
Explanation: Since reading a resource doesn't delete or modify anything, isReadOnly is true. We also declare it safe to run in parallel (isConcurrencySafe).
We need to link the tool to its validation rules. We will cover the details of these schemas in Schema Validation, but here is how we attach them.
// ... inside buildTool object
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
// ...
Explanation: The inputSchema defines what arguments the tool accepts (Server Name + URI), and outputSchema defines what the tool gives back (File Content).
call)Finally, we define what actually happens when the tool runs.
// ... inside buildTool object
async call(input, { options: { mcpClients } }) {
const { server: serverName, uri } = input
// Find the specific client the user asked for
const client = mcpClients.find(c => c.name === serverName)
if (!client) {
throw new Error(`Server "${serverName}" not found.`)
}
// ... (logic continues)
Explanation: The call function receives the validated input. We also get access to mcpClients (the list of connected servers) via the second argument context.
How does the system use this configuration object? Let's visualize the lifecycle of a tool request.
The call method performs several checks before doing the heavy lifting. This ensures reliability.
Before asking for data, we ensure the server is ready.
// ... inside call()
if (client.type !== 'connected') {
throw new Error(`Server "${serverName}" is not connected`)
}
// Check if this server actually supports 'resources'
if (!client.capabilities?.resources) {
throw new Error(`Server "${serverName}" does not support resources`)
}
Explanation: We don't just check if the server exists; we check if it is connected and if it has the capability to provide resources. We will learn more about how clients work in MCP Client Integration.
Once validated, we make the network request.
const connectedClient = await ensureConnectedClient(client)
// The core MCP SDK call
const result = await connectedClient.client.request(
{
method: 'resources/read',
params: { uri },
},
ReadResourceResultSchema,
)
Explanation: This snippet sends a message to the external server saying "Please run resources/read for this URI." It waits for the response.
The result might be text or binary data (like an image).
// ... inside call() processing results
const contents = await Promise.all(
result.contents.map(async (c, i) => {
if ('text' in c) {
return { uri: c.uri, mimeType: c.mimeType, text: c.text }
}
// ... (binary handling logic)
return { uri: c.uri, mimeType: c.mimeType }
}),
)
Explanation: We map over the results. If it's text, we return it simply. If it's binary, there is logic to save it to a file (covered in Content Persistence Strategy).
In this chapter, we created the identity and logic for the ReadMcpResourceTool. You learned how to use buildTool to configure metadata, set safety flags, and define the execution function.
However, a tool is useless if it accepts garbage inputs. How do we ensure the uri is a string and the server name is valid?
Next Chapter: Schema Validation
Generated by Code IQ