2025-06-01
2025-06-16
目次 †概要 †https://modelcontextprotocol.io/introduction [和訳] 要するにAIがLLM以外の様々なデータや機能を活用できるようにモデルとのI/F等をルール(共通)化しました。という事。
アーキ/プロトコル †TODO:
コアアーキテクチャ 基本プロトコル Go の MCPサーバ用の実装/ライブラリ †C#, Java, Python, TypeScript 等、様々な言語でSDKが公開されており、これらを利用する事でMCPサーバを構築する事が可能だが、当記事ではあえて Go で実装する。 https://pkg.go.dev/search?q=mcp 当記事では現時点で1番利用されている mark3labs/mcp-go を使用する事とする。 Github copilot 曰く MCPはIstioやEnvoyの設定配信プロトコルとして発展してきた経緯があります。 との事なので、以下のリンクを貼っておく。 MCPサーバを実装する †ここではローカル環境で動作している redmine にAPIを使用してチケットを登録するMCPサーバを書いてみた。 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サーバを起動しておく ./mymcp Clineから利用する為の設定 †ClineのMCPサーバ設定を開く [Configure MCP Servers]押下 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から利用してみる †このプロジェクト自体をVsCodeで開いて以下のように指示してみた。 しっかりとチケット登録してソースに反映してくれた模様 変更されたソース Redmineのチケット状態 |