EY-Office ブログ

MCPを学ぶためにRedmine MCPを作ってみた

最近のAIモデルの発表の頻度の勢いは凄いですね。そんな中で発表されたMCP(Model Context Protocol)はAIと外部のデータ(アプリ)と繋ぐ共通ルールとしてプログラマーの間では大きな話題になっていると感じます。
MCP自体の解説はネット上にたくさんありますし、AIに聞いてみると適切な回答が得られると思うので、ここでは省略します。

しかし、私には今一つピンと来ない部分があるので、実際にMCPを作り学んでみる事にしました。例題としてはタスク管理ソフトRedmine用のMCPを作る事にしました。私は開発タスクだけでなく、日々のToDo管理にも自前のRedmineサーバーを使ってます。

MCP 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つを作りました。

namedescription入力出力機能
get-tasksタスク一覧の取得無しsubjectの配列Redmineに登録されている全タスクのsubjectを戻す
new-taskタスク追加subjectsubjectRedmineにsubjectのタスクを登録する

これで、

  • プロンプトに「redmine-mcpを使い、タスク一覧を取得してください」と指定するとRedmineのタスク一覧が表示されます
  • プロンプトに「redmine-mcpを使い、“ミルクを買う” タスクを追加してください」と指定すると、Redmineににミルクを買うタスクが追加されます

MCPサーバーのコード (index.ts)

  • ① MCPが提供するTypeScript(JavaScript)用API
  • ② 入力値の検証等にはZodを利用しています
  • ③ Redmineの/issues.jsonAPIの戻り値の型、このコードに必要な項目しか定義していません
  • ④ 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で統合できるのは未来感がありますね。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ