2025-06-01 2025-06-16

目次

概要

https://modelcontextprotocol.io/introduction

[和訳]
MCPはアプリケーションがLLMにコンテキストを提供する方法を標準化するオープンプロトコルです。
MCPはAIアプリケーション用のUSB-Cポートのようなものだと考えてください。
USB-Cがデバイスを様々な周辺機器やアクセサリに接続するための標準化された方法を提供するのと同様に、
MCPはAIモデルを様々なデータソースやツールに接続するための標準化された方法を提供します。

要するにAIがLLM以外の様々なデータや機能を活用できるようにモデルとのI/F等をルール(共通)化しました。という事。
MCPを介する事でAIの出来る事を増やしたり、MCPから得られる情報を出力結果に反映させる等が可能となる。

  • ウェブサイトからの最新情報の取得
  • 内部資料/ドキュメントからの情報検索
  • 他アプリとの連携や外部APIを呼び出し
  • メールを送信
  • ファイル操作
  • 開発ツールの操作 等

アーキ/プロトコル

TODO:

コアアーキテクチャ
https://modelcontextprotocol.io/docs/concepts/architecture

基本プロトコル
https://modelcontextprotocol.io/specification/2025-03-26/basic

Go の MCPサーバ用の実装/ライブラリ

C#, Java, Python, TypeScript 等、様々な言語でSDKが公開されており、これらを利用する事でMCPサーバを構築する事が可能だが、当記事ではあえて Go で実装する。
Goの実装は公式では提供されていない為、サードパーティライブラリを使用する。

https://pkg.go.dev/search?q=mcp

当記事では現時点で1番利用されている mark3labs/mcp-go を使用する事とする。
https://pkg.go.dev/github.com/mark3labs/mcp-go/mcp

Github copilot 曰く

MCPはIstioやEnvoyの設定配信プロトコルとして発展してきた経緯があります。
そのため、IstioやEnvoyのGo実装の該当パッケージ・サブディレクトリが、MCPの実際の使い方や実装例として一番参考になりやすいです。
MCPそのものの「汎用Goライブラリ」は非常に少ないため、proto定義から自動生成+gRPC通信の枠組みを自分で組み合わせるのが実用的な場合も多いです。

との事なので、以下のリンクを貼っておく。
https://pkg.go.dev/github.com/istio/istio/pkg/mcp
https://github.com/istio/api/tree/master/mcp
https://pkg.go.dev/github.com/envoyproxy/go-control-plane/pkg/mcp

MCPサーバを実装する

ここではローカル環境で動作している redmine にAPIを使用してチケットを登録するMCPサーバを書いてみた。
https://redmine.jp/glossary/r/rest-api/
https://www.redmine.org/projects/redmine/wiki/rest_api

main.go

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

func main() {
	s := server.NewMCPServer(
		"My Mcp Server Demo",
		"1.0.0",
		server.WithToolCapabilities(false),
		server.WithRecovery(),
	)

	// Redmineの課題追加ツールを追加
	fmt.Println("Setting up Redmine issue tool...")
	setupAddRedmineIssueTool(s)

	// サーバー起動
	if err := server.ServeStdio(s); err != nil {
		fmt.Printf("Server error: %v\n", err)
	}
}

// RedmineのAPIキー(デフォルト)
var MY_REDMINE_API_KEY = "RedmineのAPIキー"

// Redmineの課題追加ツールを追加
// このツールはRedmineのAPIを使用して課題を追加します
func setupAddRedmineIssueTool(s *server.MCPServer) {

	addRedmineIssueTool := mcp.NewTool("add_redmine_issue",
		mcp.WithDescription("Redmineの課題を追加する"),
		mcp.WithString("apikey",
			//mcp.Required(),
			mcp.Description("Redmine用のAPIキー"),
		),
		mcp.WithString("project_id",
			mcp.Required(),
			mcp.Description("プロジェクトID"),
		),
		mcp.WithString("subject",
			mcp.Required(),
			mcp.Description("件名"),
		),
		mcp.WithString("description",
			mcp.Required(),
			mcp.Description("説明"),
		),
		mcp.WithString("priority_id",
			//mcp.Required(),
			mcp.Description("優先度(1:低め, 2:通常(default), 3:高め, 4:急いで, 5:今すぐ)"),
			mcp.Enum("1", "2", "3", "4", "5"),
		),
		mcp.WithString("assigned_to_id",
			//mcp.Required(),
			mcp.Description("担当者ユーザID"),
		),
		mcp.WithBoolean("is_private",
			//mcp.Required(),
			mcp.Description("優先度"),
		),
	)

	s.AddTool(addRedmineIssueTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {

		issue := map[string]interface{}{}

		// TODO: 取得元/方法の検討(ユーザ毎に発行されるキーだが、都度入力してもらうのは面倒)
		apiKey := request.GetString("apikey", MY_REDMINE_API_KEY)

		pjId, err := request.RequireString("project_id")
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}
		issue["project_id"] = pjId

		subject, err := request.RequireString("subject")
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}
		issue["subject"] = subject

		description, err := request.RequireString("description")
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}
		issue["description"] = description

		priorityId := request.GetString("priority_id", "2")
		if priorityId == "" {
			priorityId = "2" // デフォルトの優先度を設定
		}
		issue["priority_id"] = priorityId

		// TODO: 担当者IDは文字列のIDではなく数値(ユーザAPIを使用して文字列を数値のIDに変換する等が必要か)
		assignedToId := request.GetString("assigned_to_id", "")
		if assignedToId != "" {
			issue["assigned_to_id"] = assignedToId
		}

		isPrivate := request.GetBool("is_private", false)
		if isPrivate {
			issue["is_private"] = isPrivate
		}

		// TODO: Redmine APIのURLを何処から引っ張るか(これは入力パラメータ化するのは危険な気がする)
		url := "http://localhost:8777/issues.json"
		result, err := CallRedmineAPI(context.TODO(), "POST", url, apiKey, map[string]interface{}{"issue": issue})
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("API呼び出しに失敗: %v", err)), nil
		}

		ticketId := (result["issue"].(map[string]interface{}))["id"]
		fmt.Printf("ticketId: %v\n", ticketId)

		return mcp.NewToolResultText(fmt.Sprintf("%v", ticketId)), nil
	})
}

// Redmine APIアクセス
func CallRedmineAPI(ctx context.Context, method, url, apiKey string, payload interface{}) (map[string]interface{}, error) {

	var bodyReader io.Reader
	if payload != nil {
		b, err := json.Marshal(payload)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal payload: %w", err)
		}
		bodyReader = bytes.NewReader(b)
	}

	req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	// ヘッダにAPIキーを設定
	if apiKey != "" {
		req.Header.Set("X-Redmine-API-Key", apiKey)
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	// ステータスコードが 2xx でない場合はエラーとして扱う
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status: %s, body: %s", resp.Status, string(bodyBytes))
	}

	// JSON を map にデコード
	var result map[string]interface{}
	decoder := json.NewDecoder(resp.Body)
	if err := decoder.Decode(&result); err != nil {
		return nil, fmt.Errorf("failed to decode json: %w", err)
	}

	return result, nil
}

ビルド

go build -o mymcp main.go

(不要) MCPサーバを起動しておく
※clineが自動で起動してくれるっぽい

./mymcp

Clineから利用する為の設定

ClineのMCPサーバ設定を開く

cline_setting1.png

[Configure MCP Servers]押下

cline_setting2.png

cline_mcp_settings.json(command にはビルドしたバイナリのPATHを指定)

{
  "mcpServers": {
    "my-local-mcp": {
      "autoApprove": [
        "add_redmine_issue"
      ],
      "disabled": false,
      "timeout": 60,
      "type": "stdio",
      "command": "/path/to/mymcp",
      "args": []
    }
  }
}

[Done]押下

cline_setting3.png

Clineから利用してみる

このプロジェクト自体をVsCodeで開いて以下のように指示してみた。

cline_test1.png

しっかりとチケット登録してソースに反映してくれた模様

cline_test2.png

変更されたソース

cline_test3.png

Redmineのチケット状態

cline_test4.png

添付ファイル: filecline_test4.png 15件 [詳細] filecline_test3.png 15件 [詳細] filecline_setting2.png 15件 [詳細] filecline_test1.png 15件 [詳細] filecline_setting3.png 15件 [詳細] filecline_setting1.png 16件 [詳細] filecline_test2.png 15件 [詳細]

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2025-06-17 (火) 08:45:22 (22d)