|
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のチケット状態 |