Skip to content

Tools you didn't write (MCP)

Self-serve track. Not part of the live 90-minute block; do it any time after Foundations. Run with npm run mcp (server: npm run mcp:server).

Every tool so far has lived in your own file. Real agents pull most of their tools from elsewhere: services someone else built and deployed, that your agent connects to and calls.

In a hurry? These three steps are the whole challenge. Everything below is the why and the how.

  1. Run npm run mcp:server in one terminal (leave it running), then npm run mcp in another, and watch TripMate plan a trip but fail to book a hotel because it has no findHotel tool.
  2. Edit start/agent.ts: TODO 1 (import createMCPClient), TODO 2 (connect over HTTP and read mcpClient.tools()), TODO 3 (spread ...mcpTools into the tools object), TODO 4 (mcpClient.close() in finally).
  3. Done when “Discovered MCP tools: findHotel” prints and findHotel appears in “tools called this run” next to the local tools, with the hotel coming from the server.

The standard for that is MCP, the Model Context Protocol. An MCP server exposes a set of tools (functions you can call as the client) and your agent connects to it and uses them as if you’d written them yourself. This challenge wires one in.

You build this one in two runs. First run TripMate with only its local tools and watch it fail to book a hotel. Then connect to an MCP server and watch findHotel appear next to the local tools, with no other change to the agent. The agent loop does not change, only where one tool comes from.

Forget hotels. Connecting to any MCP server over HTTP is the same four moves, whatever it serves:

import { createMCPClient } from "@ai-sdk/mcp";

const client = await createMCPClient({                 // 1. connect to the server
  transport: { type: "http", url: "https://example.com/mcp" },
});
const remoteTools = await client.tools();              // 2. ask what it offers (AI-SDK-shaped)
const agent = new ToolLoopAgent({
  model,
  tools: { ...localTools, ...remoteTools },            // 3. spread them in beside local tools
});
// ...use the agent...
await client.close();                                   // 4. close it so the process can exit

That spread is the whole integration: a remote tool sits next to the local ones and the model cannot tell which is which. Below you write these four moves for TripMate’s hotel server.

Open start/agent.ts. It is the familiar shape: three local tools (lookupTraveler, getWeather, getFlights) and a ToolLoopAgent with a try/finally ready. You write the four moves against the findHotel server on http://localhost:4320/mcp: import (TODO 1), connect and read .tools() (TODO 2), spread ...mcpTools into the agent’s tools (TODO 3), and close() in the finally (TODO 4).

npm run mcp           # the agent
npm run mcp:server    # the server (a second terminal, leave it running)
  1. Run the agent on its own (cycle 1). No server needed yet:

    npm run mcp

    TripMate looks up the traveller, gets the weather, and prices the flight, but it has no hotel tool. It either says it cannot book a hotel or invents one, and findHotel is nowhere in “tools called this run”. That gap is what the MCP server fills.

  2. Start the MCP server in a second terminal (cycle 2). Leave it running:

    npm run mcp:server

    You should see “MCP server (findHotel) listening on http://localhost:4320/mcp”. This is a separate program, a process you did not write into your agent.

  3. Connect the client and read its tools (cycle 2, TODOs 1 and 2). Add the createMCPClient import, then write the createMCPClient call with an HTTP transport pointing at the server’s URL (port 4320), and call mcpClient.tools() to discover what the server offers. That returns AI-SDK-shaped tools, the same shape as the local ones. Log Object.keys(mcpTools) so you can see what came back. Build it from the sketch above; the TODO comments in start/agent.ts mark where each piece goes.

  4. Spread the tools in and close the client (cycle 2, TODOs 3 and 4). Add ...mcpTools to the agent’s tools object after the local tools, then call mcpClient.close() in the finally block so the process can exit. Run npm run mcp again. Now you see:

    Discovered MCP tools: findHotel
    
      [tool fired] lookupTraveler
      [tool fired] getWeather(Lisbon)
      [tool fired] getFlights(London -> Lisbon)
    
    [a trip plan, with a hotel from the server]
    
    tools called this run: lookupTraveler, getWeather, getFlights, findHotel

    findHotel has no [tool fired] line because it does not run in the agent; it runs in the server process, in the other terminal. But it shows up in “tools called”, right next to the local tools, because to the model it’s just another tool.

  5. Verify what you’ve got. Cycle 1 plans a trip but cannot book a hotel, and findHotel is not in “tools called this run”. After the four TODOs and the server running, “Discovered MCP tools: findHotel” prints, findHotel appears in “tools called”, and the trip plan includes a hotel that came from the server, not the model. You should be able to say why the agent loop did not change to add a tool from another process.

  • “Could not reach the MCP server.” You did not start it. Run npm run mcp:server in a separate terminal first, and leave it running. The starter’s catch tells you this.

  • Port 4320 in use. Something else is on it. Change PORT in mcp-server.ts and the url in agent.ts to match.

  • No [tool fired] findHotel line. Expected: that log lives in the local tools. Confirm the call in the “tools called this run” list, or in the console trace, where MCP tool calls appear like any other span.

The two paths share this one server

The MCP server does not know or care what language its clients are written in. The TypeScript agent in this folder and the Python agent in the sibling pydantic-ai path connect to the same server and call the same findHotel: a tool published once and usable by any agent in any language. Start the server once and point both paths at it.

stdio vs HTTP transport

MCP servers can be reached two main ways. stdio launches the server as a child process and talks over its standard input/output: simpler setup, but the client runs the server’s code on your machine. HTTP points at a server running independently, which is how remote, shared servers work in production: no local code execution, but your agent now depends on that external service being up. This challenge uses HTTP so the server is a real separate process you start yourself, and so both language paths can share it. The AI SDK also supports SSE for streaming remote servers.

Go deeper: real MCP servers

The interesting MCP servers are ones you would never write yourself. The GitHub MCP server exposes tools to open issues, create repos, and review pull requests; there are servers for Slack, Postgres, the filesystem, web search, and more. Matt Pocock’s AI SDK crash course wires the real GitHub server (over Docker and over HTTP) and is the best next step: see exercises/03-agents/03.05-mcp-via-stdio and 03.06-mcp-via-http. The shape is exactly what you just ran; only the server gets more powerful.

That’s tools you didn’t write, merged in with one spread. If you want to compare, npm run solution:mcp runs the finished version.