Skip to content
astorlm
Language: English
← Map

Level 10

MCP

So far every tool was a function you wrote into your own agent. MCP is a standard plug: any program can offer tools through it, and any agent can use them.
1/19 Bandoneón folds:
  • user
  • assistant
  • tool_result
Dream Land. Two MCP servers stand at their doors: weather and maps, each a program of its own. Kirby is the MCP client inside the agent, and it fits every server with the same mouth.

EventBus

The problem

Every service your agent should reach (a calendar, a repository, a database, a browser) means writing a tool by hand: its schema, its code, its error handling. Then the next agent, or the next app, writes the same tool again. And when the service changes, every copy breaks on its own.

The solution

The Model Context Protocol (MCP) separates the two sides. An MCP server is a program that offers tools; whoever builds the service writes it once. An MCP client lives inside the agent and speaks the protocol. They can run on the same machine, the server as a child process talking over stdin and stdout, or far apart, over HTTP. The conversation always has the same three steps:

  • initialize

    The handshake: the client and the server say who they are and which version of the protocol they speak.

  • tools/list

    The server describes its tools: name, description and input schema, the same three things from level 3.

  • tools/call

    Each time the model asks for one of them, the client sends its name and arguments, and gets the result back.

The tools you get are ordinary tools. They join the list that goes with every request, the loop runs them like any other, and the model can’t tell where they came from. And because the plug is standard, the same server works in Claude Code, Codex, Cursor, VS Code or your own agent.

The cast

Same cast as always, in Dream Land, with a new friend.

Kirby the MCP client
Part of your agent. One mouth that fits every server, and copies what each one can do.
The enemies MCP servers
Programs of their own, each at its own door. They go back there after Kirby copies them: they keep running.
The abilities tools/list
PARASOL and WHEEL: the tools each server described, now in the status bar the model reads.
The cables the protocol
The same kind of plug for every server. A tools/call runs down one, and its result runs back.
Astor the loop
Runs the loop as usual, and hands every MCP call to Kirby.
The Oracle the model
Picks from the toolbox by name and description, MCP or not.

The code

With astorlm: mountMcpServer() connects, lists the tools and adapts them, named <server>__<tool> so two servers can’t clash. Its tools go into the agent like any other, and close() hangs up.

From scratch: a stdio client is a child process and JSON-RPC, one JSON object per line: the handshake, tools/list, and a function per tool that sends tools/call. The tools then go into the loop from level 2.

import { OpenAIProvider, createLocalAgent, mountMcpServer } from 'astorlm'

// 1. Mount each MCP server: connect, ask for its tools (tools/list), adapt them.
//    Each one is a separate program; nobody wrote its schemas in your code.
const weather = await mountMcpServer({
  name: 'weather', // becomes the prefix: weather__forecast, weather__alerts
  transport: { type: 'stdio', command: 'npx', args: ['-y', 'dreamland-weather-mcp'] }, // runs it as a child process
})

const maps = await mountMcpServer({
  name: 'maps',
  transport: { type: 'http', url: 'https://maps.dreamland.example/mcp' }, // or a server that's already running
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  // 2. MCP tools are tools: mix them with your own, or keep only the ones you need.
  tools: [...weather.tools, ...maps.tools],
  maxTurns: 10,
})

agent.on('tool-start', ({ name }) => console.log('→', name)) // maps__route, then weather__forecast

try {
  await agent.run('Picnic at the Fountain of Dreams tomorrow. How do I get there from Cappy Town, and will it rain?')
} finally {
  // 3. Hang up when you're done: a stdio server is a process of yours.
  await Promise.all([weather.close(), maps.close()])
}

What to watch

  • Every tool rides in every request. A server with forty tools adds forty schemas to each turn, and a long list makes picking the right one harder. Mount only the servers you need, and pass on only the tools you use: weather.tools.filter(…).
  • A server is code you run. A stdio server runs with your permissions, on your machine. Treat it like a dependency: install it from a source you trust, and pin its version.
  • Descriptions are instructions too. The model reads every tool description as if you had written it, and every result as data that may carry orders. A server you don’t control can steer your agent through either (level 18).
  • Remote servers need credentials. Over HTTP, pass a token in the headers, scoped to what the agent needs, never your own all-powerful key.
  • Close what you open. A stdio server lives as long as its connection. Close it when the agent is done, or it keeps running in the background.