2025 年 5 月時点 Ollama で granite3.2 を使いツール一覧から選んで使うだけの MCP クライアントから標準入出力 MCP サーバーとつないでみるメモ
2025 年 5 月時点 Ollama で granite3.2 を使いツール一覧から選んで使うだけの MCP クライアントから標準入出力 MCP サーバーとつないでみるメモです。
背景
あくまで2025 年 5 月時点の仕様での試してみたお話です。ここから仕様が変わったり、より良いアプローチが見つかるかもしれません。
Ollama で structured outputs が理解できる granite3.2:2b のモデルの Ollama API を、自前のツール一覧から選んでツールを使うだけのシンプルな MCP クライアントのツール推論部分に使ってあげたら、ちゃんと MCP サーバーの足し算ツール実行できた!#IBMChampionForumJapan2025 #IBMChampion pic.twitter.com/WAqY1hVbmj
— Tanaka Seigo (@1ft_seabass) May 20, 2025
IBM Champion Forum Japan 2025 に参加 してインスピレーションがきたのでさっそく試してみたものです!

また、先日 2025 年 5 月時点 ツール一覧から選んでツールを使うだけのシンプルな MCP クライアントを作り標準入出力 MCP サーバーとつないでみるメモ が成功しまして、それであれば、 ChatGPT の function calling にかなり近い仕組みである、Ollama の Structured outputs にもつながるだろうなということで試してみます。
20250409_AIミーティング「Ollama の Structured outputs を IBM Granite などローカル LLM で試してみる」ところで知見があるので granite3.2:2b モデルでやってみます。
置き換えのイメージ

以前の 2025 年 5 月時点 ツール一覧から選んでツールを使うだけのシンプルな MCP クライアントを作り標準入出力 MCP サーバーとつないでみるメモ の流れから、

このように Ollama の Structured outputs で行うイメージです。
stdio 標準入出力 MCP クライアントを TypeScript で作る
GitHub Codespaces で行います

今回の仕組みは Ollama も動かすので、一旦 GitHub Codespaces 環境で進めます。
GitHub Codespaces 環境で Ollama と Granite LLM を動かすやり方は、以前も GitHub Codespaces で Ollama をインストールして軽量な TinyLlama を試してみるメモ のようなやり方や Ollama + GitHub CodeSpaces + Granite モデルを試してみようハンズオン などで試したことがあるやり方です。
フォルダ準備
mkdir mcp-client-self
まず mcp-client-self というフォルダを作ります。
cd mcp-client-self
mcp-client-self フォルダに移動します。
npm 初期設定
npm init -y
npm の初期設定をします。

このようなファイル構成になりました。
{
"name": "mcp-client-self",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
package.json ができあがりました。
"type": "module",
"scripts": {
"mcp-client-self": "node --no-warnings --loader ts-node/esm mcp-client-self.ts"
}
type module で ESM で動くようにして、scripts mpc-client-self で、このあと作成する mpc-client-self.ts のプログラムを TypeScript で動作させるコマンドを準備しておきます。
モジュールのインストール
npm install --save uuid
と、
npm install --save-dev typescript ts-node @types/node
でモジュールをインストールします。
tsconfig.json の準備
tsconfig.json もフォルダ内に準備します。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"types": ["node"],
"strict": true
},
"include": ["**/*.ts"]
}
これらの作業を終えて package.json は以下のようになりました。
{
"name": "mcp-client-self",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"mcp-client-self": "node --no-warnings --loader ts-node/esm mcp-client-self.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/node": "^22.15.18",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
mcp-client-self.ts に MCP クライアントのプログラムを作成
mpc-client-self.ts に MCP クライアントのプログラムを作成します。
内容は以下の通りです。Node.js のバージョンは fetch を動かすため
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
import * as readline from 'readline/promises';
import { v4 as uuidv4 } from 'uuid';
// ===== 型定義 =====
type JsonRpcRequest = {
jsonrpc: '2.0';
method: string;
params: any;
id: string;
};
type JsonRpcResponse = {
jsonrpc: '2.0';
result?: any;
error?: any;
id: string;
};
type RunningServer = {
proc: ChildProcessWithoutNullStreams;
write: (req: JsonRpcRequest) => Promise<JsonRpcResponse>;
};
// ===== 設定 =====
const config = {
mcpServers: {
"local-app-001": {
disabled: false,
timeout: 60,
command: "npm",
args: [
"--prefix",
"../mcp-server",
"run",
"mcp-server",
"--silent"
],
transportType: "stdio"
}
}
};
const servers: Record<string, RunningServer> = {};
// ===== MCPサーバー起動ロジック =====
function startServer(name: string, entry: any): RunningServer {
const proc = spawn(entry.command, entry.args, {
stdio: ['pipe', 'pipe', 'pipe']
});
const listeners: { [id: string]: (res: JsonRpcResponse) => void } = {};
const rlStdout = readline.createInterface({ input: proc.stdout });
rlStdout.on('line', (line: string) => {
try {
const res = JSON.parse(line) as JsonRpcResponse;
if (res.id && listeners[res.id]) {
listeners[res.id](res);
delete listeners[res.id];
}
} catch {
// 非JSON行は無視
}
});
function write(req: JsonRpcRequest): Promise<JsonRpcResponse> {
return new Promise((resolve) => {
listeners[req.id] = resolve;
proc.stdin.write(JSON.stringify(req) + '\n');
});
}
return { proc, write };
}
// ===== MCP ツールリスト取得 =====
async function listTools(serverName: string) {
const server = servers[serverName];
const request: JsonRpcRequest = {
jsonrpc: "2.0",
method: "tools/list",
params: {},
id: uuidv4()
};
const response = await server.write(request);
const result = response.result;
if (result && Array.isArray(result.tools)) {
return result.tools;
}
if (Array.isArray(result)) {
return result;
}
return [];
}
// ===== MCP ツール呼び出し =====
async function callTool(serverName: string, toolName: string, args: any) {
const server = servers[serverName];
const response = await server.write({
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: toolName,
arguments: args
},
id: uuidv4()
});
return response.result;
}
// ===== Ollama 呼び出しユーティリティ =====
async function askOllama(
userInput: string,
allToolsMap: Record<string, any[]>
): Promise<{ name: string; arguments: Record<string, any> }> {
const flatTools: { name: string; parameters: any }[] = [];
for (const [serverName, tools] of Object.entries(allToolsMap)) {
for (const tool of tools || []) {
flatTools.push({
name: `${serverName}__${tool.name}`,
parameters: tool.inputSchema
});
}
}
if (flatTools.length === 0) {
throw new Error('利用可能なツールがありません');
}
const formatSchema = {
type: 'object',
anyOf: flatTools.map((tool) => ({
type: 'object',
properties: {
name: { type: 'string', enum: [tool.name] },
arguments: tool.parameters
},
required: ['name', 'arguments']
}))
};
const body = {
model: 'granite3.2:2b',
messages: [
{
role: 'system',
content: `
あなたはMCPツール選定アシスタントです。
ツール名には "サーバー名__ツール名" の形式を使い、
実行すべきツールと引数を JSON で返してください。
JSON 以外は一切出力しないでください。
`.trim()
},
{ role: 'user', content: userInput }
],
stream: false,
format: formatSchema
};
// console.log(`body`);
// console.log(body);
const res = await fetch('http://127.0.0.1:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) {
throw new Error(`Ollama Error: ${res.status} ${await res.text()}`);
}
// ② テキストとして一度だけ読み込む
const raw = await res.text();
// console.log('返答 →', raw);
// ③ Envelope 全体をパース
const envelope = JSON.parse(raw) as {
message?: { role: string; content: string };
error?: any;
};
if (!envelope.message?.content) {
throw new Error(`Ollama レスポンスに content がありません: ${JSON.stringify(envelope)}`);
}
// ② message.content の JSON 文字列をパースして返す
try {
const picked = JSON.parse(envelope.message.content);
return {
name: picked.name,
arguments: picked.arguments
};
} catch (err) {
throw new Error(`Ollama の content パースに失敗: ${envelope.message.content}`);
}
}
// ===== ターミナル対話ループ =====
async function interactive() {
// MCPサーバー起動
for (const [name, entry] of Object.entries(config.mcpServers)) {
if (entry.disabled) continue;
const server = startServer(name, entry);
servers[name] = server;
console.log(`[OK] MCPサーバー "${name}" を起動しました`);
}
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
while (true) {
const inputText = await rl.question('\n> ');
if (inputText.trim().toLowerCase() === 'exit') break;
console.log('[…] MCPツール一覧取得中…');
const toolMap: Record<string, any[]> = {};
for (const srv of Object.keys(servers)) {
toolMap[srv] = await listTools(srv);
}
console.log('[…] Ollama granite3.2:2b に問い合わせ中…');
let picked: { name: string; arguments: Record<string, any> };
try {
picked = await askOllama(inputText, toolMap);
} catch (err) {
console.error('[X] Ollama 呼び出しエラー:', err);
continue;
}
console.log('==== 選定結果 ====');
console.log('ツール :', picked.name); // → "local-app-001__add"
console.log('引数 :', picked.arguments); // → { a: 2, b: 3 }
console.log('=================');
const confirm = await rl.question('この内容で実行しますか? (y/N) ');
if (!/^y/i.test(confirm.trim())) {
console.log('[X] キャンセルされました');
continue;
}
const [srv, tool] = picked.name.split('__');
try {
const [srv, tool] = picked.name.split('__');
const result = await callTool(srv, tool, picked.arguments);
console.log('[OK] 実行結果:', result);
} catch (err) {
console.error('[X] ツール実行エラー:', err);
}
}
rl.close();
console.log('[END] 終了');
}
interactive();
現状のファイル構成

このようなファイル構成になっています。
stdio 標準入出力 MCP サーバーを TypeScript SDK で作る
さっきは素の TypeScript で作りましたが、stdio 標準入出力 MCP サーバーは TypeScript SDK で作りました。あえて、別フォルダで MCP サーバーを作ります。
2025 年 5 月時点 stdio 標準入出力 MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携するメモ を参考に進めます。
フォルダ準備
cd ..
一旦、プロジェクト最上部に移動して、
mkdir mcp-server
mcp-server を作って、
cd mcp-server
mcp-server フォルダに移動します。
npm 初期設定
npm init -y
npm の初期設定をします。

このようなファイル構成になりました。
{
"name": "mcp-server",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
package.json ができあがりました。
"type": "module",
"scripts": {
"mcp-server": "node --no-warnings --loader ts-node/esm mcp-server.ts"
},
type module で ESM で動くようにして、scripts mpc-server で、このあと作成する mpc-server のプログラムを TypeScript SDK で動作させるコマンドを準備しておきます。
モジュールのインストール
モジュールのインストールをします。
npm install @modelcontextprotocol/sdk zod
と、
npm install -D typescript ts-node @types/node
でモジュールをインストールします。
tsconfig.json の準備
tsconfig.json もフォルダ内に準備します。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"types": ["node"],
"strict": true
},
"include": ["**/*.ts"]
}
これらの作業を終えて package.json は以下のようになりました。
{
"name": "mcp-server",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"mcp-server": "node --no-warnings --loader ts-node/esm mcp-server.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.4",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/node": "^22.15.18",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
mcp-server.ts に MCP サーバーのプログラムを作成
mcp-server.ts に MCP サーバーのプログラムを作成します。
内容は以下の通りです。
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create an MCP server
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// Add an addition tool
server.tool(
"add",
"2つの数を加算して返すシンプルなツールです",
{
a: z.number().describe("1つ目の数値"),
b: z.number().describe("2つ目の数値")
},
async ({ a, b }) => ({
content: [
{
type: "text",
text: `${a + b}`
}
]
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
自作なので、description やスキーマの説明などが伝わりやすいよう内容を厚めにしています。
現状のファイル構成

このようなファイル構成になっています。
Ollama の準備
Ollama + GitHub CodeSpaces + Granite モデルを試してみようハンズオン のやり方を参考にします。
Ollama インストール
Ollama のインストール方法 を参考に Ollama をインストールしていきます。
curl -fsSL https://ollama.com/install.sh | sh
こちらのコマンドを入力して Enter キーを押して実行してインストールします。

インストール中です。

インストールできました。
Ollama サーバー起動
インストールできたら、
ollama serve
こちらのコマンドを入力して Enter キーを押して実行して Ollama サーバーを起動します。

無事起動しました。
新しいターミナルの起動

新しいターミナルの起動します。
granite3.2:2b を動かす

granite3.2:2b をダウンロードしてモデルを動かします。(毎度、この絵がかわいい)
ollama run granite3.2:2b
こちらのコマンドを入力して Enter キーを押して実行してモデルをダウンロードして動かします。

無事動きました!
動かしてみる
では、Ollama もそろったので、動かしてみましょう。
cd ../mcp-client-self
MCP クライアントのフォルダへ移動しなおします。
npm run mcp-client-self
起動します。

入力待ちです。

2+3を足してくださいと入力して Enter キーを押します。

このように結果が出ます。 y を入力して Enter キーを押します。

無事、足し算ツールが使われて足し算された結果が表示されました!
Ollama API に送っているツール選定部分の内容

こちらの赤で囲っているツール一覧を受け取る部分のデータです。
// ===== 設定 =====
const config = {
mcpServers: {
"local-app-001": {
disabled: false,
timeout: 60,
command: "npm",
args: [
"--prefix",
"../mcp-server",
"run",
"mcp-server",
"--silent"
],
transportType: "stdio"
}
}
};
内容は Claude Desktop や Cline にある MCP サーバー設定をする JSON に近い形にしています。というかほぼ一緒です。

こちらの赤で囲っている Ollama API に送っているツール選定部分の内容は、
const body = {
model: 'granite3.2:2b',
messages: [
{
role: 'system',
content: `
あなたはMCPツール選定アシスタントです。
ツール名には "サーバー名__ツール名" の形式を使い、
実行すべきツールと引数を JSON で返してください。
JSON 以外は一切出力しないでください。
`.trim()
},
事前にこういう振る舞いで system 値で定義して、ツール一覧を渡してもらうイメージです。
以下が Ollama API http://127.0.0.1:11434/api/chat へ送っているプロンプト含めた JSON スキーマデータです。
{
"model": "granite3.2:2b",
"messages": [
{
"role": "system",
"content": "あなたはMCPツール選定アシスタントです。\nツール名には \"サーバー名__ツール名\" の形式を使い、\n実行すべきツールと引数を JSON で返してください。\nJSON 以外は一切出力しないでください。"
},
{
"role": "user",
"content": "2+3を足してください。"
}
],
"stream": false,
"format": {
"type": "object",
"anyOf": [
{
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"local-app-001__add"
]
},
"arguments": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "1つ目の数値"
},
"b": {
"type": "number",
"description": "2つ目の数値"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
"required": [
"name",
"arguments"
]
}
]
}
}
user 値には入力された内容を送って、functions 値にある function calling する中身にツール一覧を元に各ツールのスキーマを入れている流れです。
{
"model": "granite3.2:2b",
"created_at": "2025-05-20T22:15:21.529845452Z",
"message": {
"role": "assistant",
"content": "{\n \"name\": \"local-app-001__add\",\n \"arguments\": {\n \"a\": 2,\n \"b\": 3\n }\n}"
},
"done_reason": "stop",
"done": true,
"total_duration": 5002516397,
"load_duration": 7754494,
"prompt_eval_count": 80,
"prompt_eval_duration": 623712052,
"eval_count": 39,
"eval_duration": 4364961009
}
そして、返答として、このように Ollama API からデータが返答されるので、実際にツールをコールできるわけです。
いやー、Ollama の structure outputs が構造化データいけるわけだから同じようにできるかな!と試してみてうまくいったのでうれしいです!