Welcome back! In our last lesson, we explored how to define and expose your MCP server's capabilities using tools, resources, and prompts. We also practiced interacting with these primitives from a client, giving us a solid foundation for building interactive and discoverable MCP integrations.
Now we're ready to take the next step together by focusing on MCP tools and how we can use them to power real-world services. Tools are the primary way our server performs actions for clients, and understanding how to design and implement them is essential for building useful MCP applications.
For this lesson, we'll work together on building a shopping list manager as our example. This service will let us add, remove, and update items in a shopping list, just like we might do in a real app. By the end of this lesson, we'll know how to define a service in TypeScript
, expose its features as MCP tools, and connect everything so that clients can interact with our server in a meaningful way.
Let's start by understanding why tools are absolutely critical when building for AI agents. In our previous lesson, we learned that MCP servers can expose three main types of primitives: tools
, resources
, and prompts
. However, when it comes to real-world agent integrations, tools are by far the most important.
Since the MCP protocol is still relatively new, most agent applications and AI platforms currently only support the tools
primitive. This means that while resources and prompts are part of the specification, our primary focus should be on designing excellent tools if we want our MCP server to work with the majority of agent systems available today.
Think of a tool in MCP as a function or an action that our server can perform on request. For example, a tool might add two numbers, fetch a record, or update a setting. Tools are registered with the server and described using input schemas, so agents know exactly what arguments to provide and what to expect in return. This makes tools the backbone of agent-server interactions and the key to building MCP servers that agents can actually use.
In this lesson, we'll see how tools can wrap the logic of a real service — our shopping list manager — so that agents can add, remove, and update items just by calling the right tool. This tool-first approach is essential for building MCP servers that work seamlessly with today's agent ecosystem.
Imagine you have a service that manages a shopping list and you want to expose this functionality to AI agents through MCP tools. This is exactly the kind of real-world scenario where MCP shines - taking existing business logic and making it accessible to agents so they can help users add items, mark things as purchased, and manage their shopping lists.
We'll define this service in a TypeScript
class called ShoppingListService
, which will be responsible for managing our shopping list data and providing methods to interact with it. Once we have this service, we'll wrap its functionality with MCP tools to create a seamless interface for agents.
Here's our service:
Our class keeps an array of shopping items, each with an ID, name, quantity, and purchased status. The service provides methods to get all items (optionally filtered by whether they are purchased), add a new item, remove an item by ID, and mark an item as purchased or not. We're using randomUUID()
to ensure that each item has a unique identifier, which is important for tracking and updating items.
Now that we have our service, let's set up the structure for our MCP server. We'll create a function that initializes both the server and the service, then registers all our tools:
This structure gives us a clean separation between server setup and tool registration. Each time we call createMcpServer()
, we get a fresh server instance with its own service instance, which will be important when we want to support multiple clients with separate shopping lists.
Now we're ready to register each of our tools in the registerShoppingListTools
function. We'll go through them one by one, starting with the most basic tool for retrieving items, then moving on to adding, updating, and removing items. Each tool will follow the same pattern: define the tool name, provide a clear description, specify the input schema using Zod, and implement the handler function that calls our service methods.
Now we're ready to register each of our tools in the registerShoppingListTools
function. We'll start with the get_items
tool that allows clients to retrieve shopping list items with optional filtering:
Notice how we're using z.boolean().optional()
to make the purchased
parameter optional. When called without arguments, it returns all items. When called with purchased: true
or purchased: false
, it filters accordingly. We're also wrapping our service call in a try-catch block and returning a structured JSON response with success status, message, and data.
We're returning our structured data as text content using JSON.stringify()
because it's the most versatile format for our shopping list data. While the MCP SDK supports other content types like images, audio, and resource references, text is perfect for structured data that AI agents need to read and understand. The AI can directly interpret the JSON structure, understand success/failure status, and work with the shopping list data naturally without any additional processing. If you were building programmatic clients that need to manipulate this data, you'd use to convert it back to JavaScript objects, but for AI agents, the readable JSON text format is ideal.
Next, we'll register our add_item
tool that creates new shopping list items:
Here we're using z.number().positive()
to ensure the quantity is a positive number. We're also validating that our service returned a valid item with an ID before considering the operation successful. This extra validation helps us catch potential issues early.
Now we'll add our remove_item
tool that deletes items from the shopping list by ID:
This tool has a simple input schema requiring only the item ID as a string. Our service method returns a boolean indicating whether the item was found and removed, which we use to provide appropriate success or error messages.
Finally, we'll register our set_purchased
tool that updates the purchase status of items:
We're using z.boolean().default(true)
to make the purchased
parameter optional, defaulting to true
when not specified. Our response message dynamically reflects whether the item was marked as purchased or not purchased, providing clear feedback to the client.
Each of our tools follows consistent patterns for error handling, response formatting, and input validation. This consistency makes our server predictable and reliable for clients, whether they are AI agents or other applications.
Now that we have our tools registered, we need to expose them so that multiple clients can access our service. However, there's an important consideration: we want each client to have their own separate shopping list, not share a single global list. This is where stateful sessions become crucial.
Our solution is to create a separate MCP server instance for each client session. When a client connects, they get their own ShoppingListService
instance, which means their shopping list data is completely isolated from other clients. Here's how we implement this using HTTP transport:
The key insight here is our transports
object, which stores a separate transport instance for each session. Each transport is connected to its own MCP server instance, which in turn has its own ShoppingListService
instance.
Our main request handler manages session lifecycle and transport creation:
Now that we have our shopping list tools registered and exposed via HTTP transport, let's test them using an MCP client. This is essential for verifying that our tools work correctly before integrating them with AI agents or other applications.
Let's set up our test client:
Let's start by retrieving the initial shopping list that comes pre-populated with sample data:
This produces output showing our default items:
Let's test our add_item
tool by adding bananas to the shopping list:
The output shows our newly created item with its generated ID:
Notice how we extract the item ID from the response - this is important for subsequent operations that need to reference this specific item.
Now let's test our set_purchased
tool to mark our bananas as purchased:
The output confirms the purchase status was updated:
Finally, let's test our remove_item
tool to clean up our test data:
The output shows successful removal:
This comprehensive test demonstrates that all four of our tools work correctly: get_items
retrieves data, add_item
creates new items, set_purchased
updates item status, and remove_item
deletes items. The consistent JSON response format makes it easy to parse results and handle both success and error cases programmatically.
In this lesson, we've built a complete MCP service together! We started by defining a service in TypeScript
to manage our application's data, then exposed its features as MCP tools using registerTool
with proper input schemas and error handling. We connected each tool to a method on our service, creating a clean and maintainable way for clients to interact with our server.
We also implemented stateful HTTP transport so multiple clients can each have their own separate shopping lists, and we tested everything using an MCP client to verify our tools work correctly.
The key insights we've covered together are:
- Tools are the most important MCP primitive for current agent integrations
- Service separation keeps our business logic clean and testable
- Consistent response formatting makes our server predictable and reliable
- Session-based server instances enable multi-client support with data isolation
- Comprehensive testing ensures our tools work before production use
Next, you'll get to implement and test these concepts yourself through hands-on practice exercises. You'll build your own MCP service tools and see how they integrate with real clients. Keep experimenting and exploring — this hands-on approach is the best way to master MCP tools and build useful, interactive services!
