iTranslated by AI
Building a Free MCP Agent with Ollama
Creating a Local MCP Agent
In this article, we will run an MCP Agent using a local LLM via Ollama. We use an OpenAI-compatible API. This setup has the advantage of incurring no costs other than electricity during development.
- Sample code and operation screen for this project

Creating a Custom Transport to Connect McpServer Directly to McpClient
When using @modelcontextprotocol/sdk for MCP communication, the standard Transports provided to handle an McpServer via an McpClient are limited to stdio, websocket, and http. Since setting up separate processes or starting a WebServer can make development cumbersome, we will create a custom Transport to connect them directly.
- libs/direct-transport.ts
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
class DirectClientTransport implements Transport {
onclose?: () => void;
onmessage?: (message: JSONRPCMessage) => void;
constructor(private serverTransport: Transport) {}
async start() {}
async close() {}
async send(message: JSONRPCMessage) {
this.serverTransport.onmessage?.(message);
}
}
export class DirectServerTransport implements Transport {
onclose?: () => void;
onmessage?: (message: JSONRPCMessage) => void;
clientTransport: DirectClientTransport;
constructor() {
this.clientTransport = new DirectClientTransport(this);
}
async start() {}
async close() {}
async send(message: JSONRPCMessage) {
this.clientTransport.onmessage?.(message);
}
getClientTransport() {
return this.clientTransport;
}
}
Creating MCP Servers
We will create an McpServer that returns the current time and another that returns the weather forecast for a specified location.
- mcp-servers/get-current-time.ts
It simply returns the time.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "時間表示サーバー",
version: "1.0.0",
});
server.tool("get-current-time", "現在の時刻を返す", async () => {
return {
content: [
{
type: "text",
text: new Date().toLocaleString("ja-JP", {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
},
],
};
});
export const TimeServer = server;
- mcp-servers/get-weather.ts
Fetches weather information text from the Japan Meteorological Agency (JMA) website.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
interface Center {
name: string;
enName: string;
officeName?: string;
children?: string[];
parent?: string;
kana?: string;
}
interface Centers {
[key: string]: Center;
}
interface Area {
centers: Centers;
offices: Centers;
class10s: Centers;
class15s: Centers;
class20s: Centers;
}
interface Weather {
publishingOffice: string;
reportDatetime: Date;
targetArea: string;
headlineText: string;
text: string;
}
const server = new McpServer({
name: "天気予報サーバー",
version: "1.0.0",
});
server.tool(
"get-weather",
`指定した都道府県の天気予報を返す`,
{
name: z.string({
description: "都道府県名の漢字、例「東京」",
}),
},
async ({ name: areaName }) => {
const result = await fetch(
"https://www.jma.go.jp/bosai/common/const/area.json"
)
.then((v) => v.json())
.then((v: Area) => v.offices)
.then((v: Centers) =>
Object.entries(v).flatMap(([id, { name }]) =>
name.includes(areaName) ? [id] : []
)
);
const weathers = await Promise.all(
result.map((id) =>
fetch(
`https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${id}.json`
)
.then((v) => v.json())
.then((v: Weather) => v.text)
)
);
return {
content: [
{
type: "text",
text: weathers.join("---"),
},
],
};
}
);
export const WeatherServer = server;
Creating the MCP Agent
The essential part of an MCP Agent is collecting tool information from the McpServer. In getMcpTools, we create an McpClient, connect it to the McpServer, and extract the available tools.
Next, we pass the extracted tools to openai.chat.completions.create to list the necessary tool calls for the response. Based on the information in content.message.tool_calls, we invoke mcp.callTool.
After storing the tool execution results in the messages array, we generate the final response again using openai.chat.completions.create.
Occasionally, the model might generate nonsensical responses, so it may be necessary to implement a structure that validates the results and retries if they are not satisfactory.
When searching for an LLM to use as an agent in Ollama, you'll need one with the tools tag. Furthermore, even among models with tool support, some have an unusually high failure rate for tool usage, so it's important to find the most suitable model for your needs.
- index.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import OpenAI from "openai";
import { DirectServerTransport } from "./libs/direct-transport.js";
import { TimeServer } from "./mcp-servers/get-current-time.js";
import { WeatherServer } from "./mcp-servers/get-weather.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type {
ChatCompletionContentPartText,
ChatCompletionMessageParam,
ChatCompletionTool,
} from "openai/resources.mjs";
const getMcpTools = async (servers: McpServer[]) => {
const tools: ChatCompletionTool[] = [];
const functionMap: Record<string, Client> = {};
const clients: Client[] = [];
for (const server of servers) {
const mcpClient = new Client({
name: "mcp-client-cli",
version: "1.0.0",
});
// Connecting McpServer directly to McpClient
const transport = new DirectServerTransport();
server.connect(transport);
await mcpClient.connect(transport.getClientTransport());
clients.push(mcpClient);
const toolsResult = await mcpClient.listTools();
tools.push(
...toolsResult.tools.map((tool): ChatCompletionTool => {
functionMap[tool.name] = mcpClient;
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
},
};
})
);
}
const close = () => {
return Promise.all(
clients.map(async (v) => {
await v.close();
})
);
};
return { tools, functionMap, close };
};
const query = async (
openai: OpenAI,
model: string,
mcpTools: Awaited<ReturnType<typeof getMcpTools>>,
query: string
) => {
console.log(`\n[question] ${query}`);
const messages: ChatCompletionMessageParam[] = [
{
role: "system",
content: "日本語を使用する,タグを出力しない,plain/textで回答する",
},
{
role: "user",
content: query,
},
];
const response = await openai.chat.completions.create({
model,
messages: messages,
tools: mcpTools.tools,
});
for (const content of response.choices) {
if (content.finish_reason === "tool_calls" && content.message.tool_calls) {
await Promise.all(
content.message.tool_calls.map(async (toolCall) => {
const toolName = toolCall.function.name;
const toolArgs = toolCall.function.arguments;
const mcp = mcpTools.functionMap[toolName];
console.info(`[tool] ${toolName} ${toolArgs}`);
if (!mcp) {
throw new Error(`Tool ${toolName} not found`);
}
const toolResult = await mcp.callTool({
name: toolName,
arguments: JSON.parse(toolArgs),
});
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: toolResult.content as Array<ChatCompletionContentPartText>,
});
})
);
const response = await openai.chat.completions.create({
model,
messages,
max_completion_tokens: 512,
stream: true,
});
console.log("[answer]");
for await (const message of response) {
process.stdout.write(message.choices[0].delta.content!);
}
console.log();
} else {
console.log(content.message.content);
}
}
};
async function main() {
const openai = new OpenAI({
baseURL: "http://localhost:11434/v1",
apiKey: "ollama",
});
const mcpTools = await getMcpTools([TimeServer, WeatherServer]);
const model = "qwen2.5-coder:14b";
await query(openai, model, mcpTools, "東京の天気は?");
await query(openai, model, mcpTools, "今日の青森と千葉の天気は?");
await query(openai, model, mcpTools, "今日は何曜日?");
await mcpTools.close();
}
main();
Execution Results
- Successful response
[question] What is the weather in Tokyo?
[tool] get-weather {"name":"Tokyo"}
[answer]
It seems like it's currently cloudy in Tokyo.
There is a possibility of occasional rain through the 31st, so please don't forget your umbrella.
Also, on April 1st, rain or snow is expected mainly around the Izu Islands, so special caution is required in those areas.
[question] What is the weather in Aomori and Chiba today?
[tool] get-weather {"name":"Aomori"}
[tool] get-weather {"name":"Chiba"}
[answer]
Aomori Prefecture is cloudy or sunny, with snow falling in some areas. There is a possibility of rain on the 31st, and while it will be sunny on April 1st, it is expected to become cloudy in the afternoon.
Chiba Prefecture is cloudy with rain in some areas. Cloudy and rainy weather may continue on the 31st, and rain is expected on April 1st. Additionally, caution is advised on the Pacific coast of Chiba Prefecture, as waves will be high on the 31st.
[question] What day of the week is it today?
[tool] get-current-time {}
[answer]
Monday
- Successful response
[question] What is the weather in Tokyo?
[tool] get-weather {"name":"Tokyo"}
[answer]
The current weather in Tokyo is cloudy.
On the 31st, it will be affected by high pressure, but due to atmospheric troughs and moist air, there is a possibility of rain from the afternoon.
On April 1st, the high pressure centered over the Kuril Islands will move northeast, bringing rain. There is also a high possibility of rain with thunder in the Izu Islands, so precautions are necessary when going out.
In the Kanto-Koshin region as a whole, it will be cloudy or sunny, with rain or snow in some places. At sea, caution is required for vessels as waves will be high on the 31st and rough on April 1st.
[question] What is the weather in Aomori and Chiba today?
[tool] get-weather {"name":"Aomori"}
[tool] get-weather {"name":"Chiba"}
[answer]
Aomori Prefecture:
- Today: Cloudy or sunny, with snow in some areas
- Tomorrow: Sunny, but may become cloudy from the afternoon
Chiba Prefecture:
- Today: Cloudy with rain in some areas
- Tomorrow: Rain expected
[question] What day of the week is it today?
[tool] get-current-time {}
[answer]
Today is Monday.
- The tool call content somehow appears on the content side
[question] What is the weather in Tokyo?
[tool] get-weather {"name":"Tokyo"}
[answer]
Currently, the weather in Tokyo is cloudy.
On the 31st, it will be cloudy with a possibility of rain from the afternoon.
On April 1st, high pressure centered to the east of the Kuril Islands is expected to move, resulting in rain. Rain with thunder is also possible in the Izu Islands.
[question] What is the weather in Aomori and Chiba today?
[tool] get-weather {"name":"Aomori"}
[tool] get-weather {"name":"Chiba"}
[answer]
The weather forecast for Aomori and Chiba prefectures is as follows:
### Aomori Prefecture
- **31st**: Due to being covered by high pressure, it will be sunny or cloudy, but there is a possibility of snow until before noon.
- **April 1st**: Due to high pressure, it is expected to be cloudy in the afternoon.
### Chiba Prefecture
- **31st**: Due to the influence of atmospheric troughs and moist air, it will be cloudy with rain in some areas.
- **April 1st**: High pressure centered to the east of the Kuril Islands will move northeast, and rain is expected.
In the coastal waters of the Pacific side of Chiba Prefecture:
- **31st**: There is a possibility of high waves with swells.
- **April 1st**: It is expected to become rough with swells. Vessels should be careful of high waves.
[question] What day of the week is it today?
{
"name": "get-current-time",
"arguments": null
}
Summary
In the future, I believe we will see more and more cases where APIs for various services are converted into McpServers. When that task comes your way, it is convenient to have an environment ready where you can quickly verify operations locally.
Discussion