mcp-server-redmineを使ってチケットを検索する

MCP とはAnthropicがリリースしたLLMアプリケーションがコンテキストを提供する方法を標準化したもの。

MCPはAIアプリケーションのUSB-Cポートのようなものだと考えてほしい。

Tool CallのTool部分を切り出したもの。

  • MCP server: Toolを提供する側
  • MCP client: Toolを利用する側=LLM

MCP serverをまとめているリポジトリがある modelcontextprotocol/servers

タスク

redmineの以下のチケットコメントに記載されている旅程を調べてDiscordに投稿するLLM Agentを作成してみる。

岐阜・滋賀旅行のチケット

システムプロンプトは無駄な往復を避けるために以下のように設定した。

あなたは秘書です。

- 質問に対してツールを必ず使い、redmineのプロジェクトID:3から情報を取得してください
- 最初limitは30にしてください。
- チケットのコメント(journals)も確認してください
- 結果はdiscordに送信してください
- discordサーバーはplayground、チャンネルは一般(ID:804364635800076371)を使用してください

MCP Serverの準備

Redmine MCP serverにToolを追加する

mcp-server-redmine はまだ実装中らしく, チケット操作の機能についてはチケット一覧しかない。

なので、検索、チケット取得のToolを実装した。(MCPのTool定義, Toolのハンドラ定義, Redmine API clientの実装が綺麗に分かれているので追加しやすかった)

  • Tool定義
export const ISSUE_SEARCH_TOOL: Tool = {
  name: "search_issue",
  description: "Search issue. " +
    "Set query to search issues" +
    "Available since Redmine 3.3",
  inputSchema: {
    type: "object",
    properties: {
      q: {
        type: "string",
        description: "Query string to search issues",
      },
      // Pagination
      offset: {
        type: "number",
        description: "Number of issues to skip",
        default: 0,
        minimum: 0,
      },
      limit: {
        type: "number",
        description: "Maximum issues to return, from 1 to 100",
        minimum: 1,
        maximum: 100,
        default: 25,
      },
    },
    required: ["q"],
  },
};
  • Toolのハンドラ定義
const server = new Server(...);
 
const context: HandlerContext = {
  client: redmineClient,
  config: config,
};
 
const issuesHandlers = createIssuesHandlers(context);
// ...
 
const handlers = {
  ...issuesHandlers,
  // ...
};
 
const TOOLS: Tool[] = [
  tools.ISSUE_LIST_TOOL,
  tools.ISSUE_SHOW_TOOL,
  tools.ISSUE_SEARCH_TOOL,
  tools.ISSUE_CREATE_TOOL,
  tools.ISSUE_UPDATE_TOOL,
  tools.ISSUE_DELETE_TOOL,
  // ...
];
 
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    // ...
    return await handlers[name as keyof typeof handlers](args);
});
type ToolResponse = {
    content: { type: string, content: string }[]
    isError: boolean
};
 
export function createIssuesHandlers(context: HandlerContext) {
  const { client } = context;
 
  return {
    show_issue: async (args: Record<string, unknown>): Promise<ToolResponse> => {
      try {
        const id = asNumber(args.id);
        // チケットコメント(journals)を含める
        const { issue } = await client.issues.getIssue(id, { include: "journals" });
        return {
          content: [{ type: "text", text: formatIssue(issue) }],
          isError: false,
        };
      } catch (error) {
        return {
          content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
          isError: true,
        };
      }
    },
    // ...
  }
}

discord-mcp-server

今回はメッセージの送信のためだけに使用。

  1. Discord developer portalでBotを作成
  2. mcp-discord
    • 少し直せば動いた

MCP Clientでタスクを実行する

MCP client① Roo Code

> 岐阜旅行の旅程は?

ユーザーの指示を確認します:

秘書として振る舞う
ツールを使用してRedmineのプロジェクトID:3から情報を取得する
最初のlimitは30に設定する
チケットのコメント(notes)も確認する
結果をDiscordに送信する(サーバー:playground、チャンネル:一般、ID:804364635800076371)
ユーザーの質問は「岐阜旅行の旅程は?」です。これはRedmineのプロジェクトID:3のチケットから岐阜旅行に関する情報を取得し、その旅程を確認する必要があります。

まず、RedmineからプロジェクトID:3の情報を取得するために、MCP Serverのツールを使用します。このプロジェクトでは「mcp-server-redmine」が利用可能です。

最初に、プロジェクトID:3の詳細情報を確認しましょう。

---
Roo wants to use a tool on the mcp-server-redmine MCP server:
show_project { "id": 3 }
---

プロジェクトID:3の情報を取得しました。プロジェクト名は「人生」で、識別子は「life」です。このプロジェクトは非公開(is_public: no)で、アクティブな状態です。

次に、このプロジェクト内のチケット(issues)を検索して、岐阜旅行に関する情報を探します。ユーザーの指示に従い、limitを30に設定します。

---
Roo wants to use a tool on the mcp-server-redmine MCP server:
list_issues { "project_id": "3", "limit": 30 }
---

チケットのリストを取得しました。その中で、岐阜旅行に関連するチケットを見つけました:

ID: 114
タイトル: 2025/3/15-16 岐阜・滋賀旅行
ステータス: In Progress
カテゴリ: イベント
期限日: 2025-03-16

このチケットの詳細情報とコメント(notes)を確認するために、show_issueツールを使用します。

---
Roo wants to use a tool on the mcp-server-redmine MCP server:
show_issue { "id": "114" }
---

岐阜旅行に関するチケットの詳細情報とコメントを取得しました。(以下、旅程の詳細)

この情報をDiscordに送信します。Discordサーバー「playground」、チャンネル「一般」(ID: 804364635800076371)を使用します。

---
Roo wants to use a tool on the mcp-discord MCP server:
send_message { "channel_id": "804364635800076371", "content": "..." }
---

Discordへのメッセージ送信が成功しました。

---
Task Completed
---

Discordに投稿された

タスクの消費トークン数等

MCP client② Goose

Rust製。

LLM Providerを設定した後に、goose configure でMCP server名と実行コマンド, 環境変数をプロンプトで入力。

$ goose configure
T   goose-configure
|
o  What would you like to configure?
|  Add Extension
|
o  What type of extension would you like to add?
|  Command-line Extension
|
o  What would you like to call this extension?
|  mcp-server-redmine
|
o  What command should be run?
|  npx -y --prefix /home/kmt/Documents/mcp-server-redmine mcp-server-redmine
|
o  Would you like to add environment variables?
|  Yes
|
o  Environment variable name:
|  REDMINE_HOST
|
o  Environment variable value:
|  ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
|
o  Add another environment variable?
|  Yes
|
o  Environment variable name:
|  REDMINE_API_KEY
|
o  Environment variable value:
|  ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
|
o  Add another environment variable?
|  No
|
  Added mcp-server-redmine extension
  • Toolのschemaに oneOf とかがあるとエラーになる様子。
    • プロトコルの仕様に準拠しているかは不明
// ERROR goose::agents::truncate: Error: Request failed: Request failed with status: 400 Bad Request. Message: tools.21.custom.input_schema: input_schema does not support oneOf, allOf, or anyOf at the top level
 
TOOL = {
  inputSchema: {
    type: "object",
    properties: { ... },
    oneOf: [
      { required: ["project_id"] },
      { required: ["issue_id"] },
    ],
  },
}

実行結果

  • Claude 3.5: 明示的に「チケットから探して」と指示しないとredmineを使ってくれない。
goose session
starting session | provider: anthropic model: claude-3-5-sonnet-latest

( O)> 岐阜旅行の旅程は?
申し訳ありませんが、現在のツールセットでは直接旅行の旅程を確認する機能はありません。

利用可能なツールは以下のようなものです:
- Redmine(プロジェクト管理システム)関連の機能
- ファイル操作やシェルコマンド実行などの開発者向け機能

もし旅行の旅程がRedmineのチケットやプロジェクトとして管理されている場合は、それを検索して確認することがで
きます。Redmineで確認したい場合は、プロジェクトIDやキーワードなどの検索条件を教えていただけますでしょうか?
  • Claude 3.7: 何も言わなくてもredmineを使ってくれる。
starting session | provider: anthropic model: claude-3-7-sonnet-20250219

( O)> 岐阜旅行の旅程は?
岐阜旅行の旅程について調べるために、Redmineのプロジェクトから情報を確認します。プロジェクトID:3の情報を見
てみましょう。

─── list_issues | mcp-server-redmine ──────────────────────────
limit: 30
project_id: 3

岐阜旅行に関連するチケットを見つけました。ID:114「2025/3/15-16 岐阜・滋賀旅行」について詳しく確認します。

─── show_issue | mcp-server-redmine ──────────────────────────
id: 114

岐阜旅行の情報が見つかりました。これをDiscordに送信します。
(以下、旅程を整形して出力)

MCP client③ mcp-client-cli

Python製。動かない。

adhikasp/mcp-client-cli (mcp-client-cli)

pip install git+https://github.com/adhikasp/mcp-client-cli.git
code ~/.llm/config.json
{
    "systemPrompt": "You are an AI assistant helping a software engineer...",
    "llm": {
        "provider": "openai",
        "model": "gpt-4o-mini",
        "api_key": "sk-***"
    },
    "mcpServers": {
        "mcp-server-redmine": {
            "command": "npx",
            "args": [
                "-y",
                "--prefix",
                "/home/kmt/Documents/mcp-server-redmine",
                "mcp-server-redmine"
            ],
            "env": {
                "REDMINE_HOST": "https://...",
                "REDMINE_API_KEY": "***"
            }
        },
        "discord": {
            "command": "uv",
            "args": [
                "--directory",
                "/home/kmt/Documents/mcp-discord",
                "run",
                "mcp-discord"
            ],
            "env": {
                "DISCORD_TOKEN": "***"
            }
        }
    }
}