最近のAIモデルの発表の頻度の勢いは凄いですね。そんな中で発表されたMCP(Model Context Protocol)はAIと外部のデータ(アプリ)と繋ぐ共通ルールとしてプログラマーの間では大きな話題になっていると感じます。
MCP自体の解説はネット上にたくさんありますし、AIに聞いてみると適切な回答が得られると思うので、ここでは省略します。
しかし、私には今一つピンと来ない部分があるので、実際にMCPを作り学んでみる事にしました。例題としてはタスク管理ソフトRedmine用のMCPを作る事にしました。私は開発タスクだけでなく、日々のToDo管理にも自前のRedmineサーバーを使ってます。
Bing Image Creatorが生成した画像を使っています
MCPの作り方をAI(Perplexity)に聞いてみた
今回は、なるべくAIに聞いてコードを書いてみる事にしました、無料で使えるPerplexityに「node.jsを使ってMCPを作る手順を教えてください」と訪ねると以下のような回答がありました。
Node.jsを使ってMCP(Model Context Protocol)サーバーやクライアントを作る手順は、公式SDKやCLIツールを活用することで比較的簡単に行えます。以下に、MCPサーバーとクライアントの基本的な作成手順をまとめます。
MCPサーバーの作成手順
1. プロジェクトの準備
- 新しいディレクトリを作成し、Node.jsプロジェクトを初期化します。
mkdir my-mcp-server
cd my-mcp-server
npm init -y
2. 必要なパッケージのインストール
- MCP SDKやTypeScriptなど必要な依存パッケージをインストールします。
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
3. TypeScript設定ファイルの作成
npx tsc --init
4. サーバー実装ファイルの作成
src/index.ts
などにサーバーのロジックを記述します。以下は最小構成の例です。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
tools: [
{
name: "hello",
description: "挨拶を返すツール",
inputSchema: { type: "object", properties: {} },
execute: async () => ({ message: "こんにちは、MCPサーバーです!" }),
},
],
});
server.listen(new StdioServerTransport());
5. ビルドと実行
- TypeScriptをビルドし、Node.jsで実行します。
npx tsc
node build/index.js
少し手順が抜けていましたが、これを基に試したのですがVS Code上のTypeScriptがエラーを出しています。ネットで調べるとサーバーのコードが変です。古バージョン用のコードなのでしょうか?
そこで諦め、ネット上の情報をめMCPのGitHubにある、TypeScript用APIのMCP TypeScript SDKのQuick Startページを見つけ、試したところ足し算のMCPが動きました❗
動いたコード
import { McpServer } 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",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
Redmine MCP
今回はMCPのTools (モデルが操作を行ったり、情報を取得したりできるようにする実行可能な関数)として2つを作りました。
name | description | 入力 | 出力 | 機能 |
---|---|---|---|---|
get-tasks | タスク一覧の取得 | 無し | subjectの配列 | Redmineに登録されている全タスクのsubjectを戻す |
new-task | タスク追加 | subject | subject | Redmineにsubjectのタスクを登録する |
これで、
- プロンプトに「redmine-mcpを使い、タスク一覧を取得してください」と指定するとRedmineのタスク一覧が表示されます
- プロンプトに「redmine-mcpを使い、“ミルクを買う” タスクを追加してください」と指定すると、Redmineににミルクを買うタスクが追加されます
MCPサーバーのコード (index.ts)
- ① MCPが提供するTypeScript(JavaScript)用API
- ② 入力値の検証等にはZodを利用しています
- ③ Redmineの
/issues.json
APIの戻り値の型、このコードに必要な項目しか定義していません - ④ RedmineサーバーのURLやAPIキーは環境変数から読み込んでいます
- 今回クライアントに使ったRoo Codeでは環境変数の設定はMCP設定ファイルで指定します(Cursorも同様ですね)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // ← ↓ ①
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; // ← ②
type RedmineIssueSubType = { // ← ③
id: number;
project: {
id: number;
name: string
}
subject: string;
}
const REDMINE_API_KEY = process.env["REDMINE_API_KEY"]; // ← ④
const REDMINE_SERVER_URL = process.env["REDMINE_SERVER_URL"];
const REDMINE_PROJECT_ID = 1;
async function getTasks() { // ← ⑤
const issuesUrl = `${REDMINE_SERVER_URL}/issues.json?project_id=${REDMINE_PROJECT_ID}`;
const response = await fetch(issuesUrl,
{ headers: {
"Content-Type": "application/json",
"X-Redmine-API-Key": REDMINE_API_KEY
} as HeadersInit // ← ⑥
});
const tasks = await response.json();
const subjects = tasks.issues.map((t:RedmineIssueSubType) => t.subject);
return subjects;
}
async function postTask(subject: string) { // ← ⑦
const issuesUrl = `${REDMINE_SERVER_URL}/issues.json`;
const issueData = {
issue: {
subject,
project_id: REDMINE_PROJECT_ID
}
};
await fetch(issuesUrl,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Redmine-API-Key": REDMINE_API_KEY
} as HeadersInit,
body: JSON.stringify(issueData)
});
}
const server = new McpServer({ // ← ⑧
name: "redmine-mcp",
version: "1.1.3"
});
server.tool("get-tasks", // ← ⑨
"タスク一覧の取得",
{},
async ({}) => {
const subjects = await getTasks();
return {
content: [{type: "text", text: JSON.stringify(subjects)}]
};
});
server.tool("new-task", // ← ⑩
"タスク追加",
{subject: z.string()},
async ({subject}) => {
await postTask(subject);
return {
content: [{type: "text", text: subject}]
};
});
const transport = new StdioServerTransport(); // ← ⑪
await server.connect(transport);
- ⑤ Redmineの
GET /issues.json
APIでタスク情報を取得- 通信エラー等のコードはありません 🤫
- ⑥ fetch関数のheadersオプションで非標準のヘッダー名を指定する場合はHeadersInit型にします。Stack overflowで知りました❗
- ⑦ Redmineの
POST/issues.json
APIでタスクの登録- 通信エラー等のコードはありません 🤫
- ⑧ MCPサーバーの初期化
- ⑨ get-tasks Toolの設定
- ⑩ new-task Toolの設定
- ⑪ 標準入出力(STDIO)接続でサーバーを起動、クライアントと同じマシンでサーバーが動かす必要があります
開発環境の設定ファイル
- tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
- package.jsonの変更
{
・・・ // npm init で作成された部分
"type": "module",
"scripts": {
"build": "tsc && chmod 755 build/index.js"
}
}
MCPのデバッグ
MCPサーバーのテスト・デバッグですが、MCP Inspectorが便利です。ターミナルでnpx @modelcontextprotocol/inspector node build/index.js
と実行するとMCPの動くProxy serverが起動され、以下の画像のようにブラウザーからTools等が直接実行できます。
また、console.error()
でログが出力できます(画像の左下の赤い部分)。
もちろん、コアになるロジックや通信コードは単体でテストしておいた方が良いでしょう。
まとめ
動くまでに1日近くかかってしまいました
まず、無料AI(Perplexity)の教えてくれたコードは間違っていました。
そこで、今まで通りネットで検索していろいろな記事を読んで・・・試し・・・公式ドキュメントにたどり着く。といういつものパターンでMCPが動くようなりました。
後で有料AI(Claude Sonnet 4)に聞いたところ正しいコードが出てきました。人間でもそうですが、聞く人を間違うと時間や労力をロスしますね。😅
MCPは簡単に作れる、未来感あるかも
完成してみるとMCPはシンプルなコードで実現できる事が判りました。ただし、プロンプトからは「タスク一覧を取得してください」でも「全タスクを取得してください」でもget-tasksが動くのはAIの良いところですね。
現在いろいろなサービスやアプリがMCPをサポートし出し始めていますね。独自のシステムやサービスにもMCPを用意すれば、仕事で使っている複数のシステムやサービスを、ロジックやルールを含む自然言語のAIで統合できるのは未来感がありますね。