RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

mcp-goで作って学ぶMCPサーバー

はじめに

こんにちは、エンジニア3年目のTKDSです! 最近MCPが盛り上がってます。
流れに乗ってGoでやる方法を調べて試してみました!
まず簡単に現在時刻を返すMCPサーバーを作ったあと、割と実用的に使えそうなファイルを連結して返すMCPサーバーを作っていきます。
今回書いたコードのリポジトリです。
https://github.com/tkeshun/mcp

MCPとは?

MCPのドキュメントによると

MCP is an open protocol that standardizes how applications provide context to LLMs.

要するに、LLMが外部リソースとアクセスする約束事を決めてくれたみたいです。 MCPについての詳細はは他に詳しい記事がたくさんあると思うのでそちらに譲ります。
では早速つくっていきましょう!

今回使用するライブラリ

今回使用するライブラリはmcp-goです。
github mcp serverで使われてたので採用しました。
go.mod https://github.com/mark3labs/mcp-go
では、実装に移っていきます。

現在時刻を返すMCPサーバー

今回はサーバー経由で計算して結果を返すのでtoolsを使います。
toolsには、利用可能なツール一覧を表示するtools/listと実際に叩かれるtools/callが必要なようです。

Discovery: Clients can list available tools through the tools/list endpoint
Invocation: Tools are called using the tools/call endpoint, where servers perform the requested operation and return results

mcp-goではmcp.NewTool関数を使えば作れそうです。
では実際に作ってみましょう。
Goでの準備方法書いてますが知ってる人は飛ばしてOKです。

1. プロジェクトの準備

mkdir mcp-time-server
go mod init mcp-time-server

2.コード作成

以下のような感じです。
サンプルを参考に書きました。
AddToolsの定義見た感じ、処理の関数はserver.ToolHandlerFunc型を満たしていればよさそうです。
まず、NewMCPServerでMCPサーバーを作ります。
次に、mcp.NewToolで登録するtoolの素を作ります。
そして、s.AddToolでtoolと処理を紐付けて登録します。
最後に標準出力を受け付けるようにサーバー起動します。

package main

import (
    "context"
    "fmt"
    "time"

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

func main() {
    s := server.NewMCPServer("時刻答える君", "0.0.1")

    currentTimeTool := mcp.NewTool("current_time",
        mcp.WithDescription("現在時刻を返します"),
    )

    s.AddTool(currentTimeTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        jst := time.FixedZone("Asia/Tokyo", 9*60*60)
        now := time.Now().In(jst)
        message := fmt.Sprintf("現在の時刻: %s", now.Format("2006-01-02 15:04:05"))
        return mcp.NewToolResultText(message), nil
    })

    if err := server.ServeStdio(s); err != nil {
        fmt.Printf("サーバーエラー: %v\n", err)
    }
}

3. パッケージのダウンロードとビルド

MCPサーバーを起動したときのPathの関係とかがよくわからないので、今回はビルドしてMCPクライアントを起動する場所におきました!

go mod tidy
go build -o current-time-server

4. 試す

inspectorを使います。

README.mdに載ってるconfig.jsonをもとに適当に書き換えました。
一回起動してみると、everythingが選択肢にあったのでおそらくそこがサーバー名です。なので、そこ以下の値を書き換えていきます。
項目はGithub Copilot Chatの設定と変わらないので、特に迷うことはないと思います。
commandに起動するバイナリ名、argに引数、envに環境変数を書きます。項目名にちょっとDockerfileっぽさを感じます。
今回はcommandだけで大丈夫です。

{
    "mcpServers": {
      "current-time": {
        "command": "current-time-server",
        "args": [],
        "env": {
        }
      }
    }
}

ファイルの配置はこんな感じです。

. - current-time-server(Goのバイナリ)  
| - config.json  

では起動します。
npx @modelcontextprotocol/inspector --config ./config.json --server current-time で起動できます。 ↓出力

$ npx @modelcontextprotocol/inspector --config ./config.json --server current-time
Starting MCP inspector...
⚙️ Proxy server listening on port 6277
🔍 MCP Inspector is up and running at http://127.0.0.1:6274 🚀

起動するとこんな画面になります。

Connectを押して、List Toolsボタンを押すと、以下のような画面になります。
tools/listエンドポイントを叩いて、ツール一覧を取得してるようです。
表示されるメッセージは、

 currentTimeTool := mcp.NewTool("current_time",
        mcp.WithDescription("現在時刻を返します"),
    )

に書いたやつなので、mcp-goでは、mcp.NewToolの宣言時に書いたものがlistで返される値になるようです。

current_timeを押すと、以下のようにRun Toolボタンが表示されます。
これを押すとmcpサーバーが実行されます。

無事現在時刻が表示されました!👏

以上がMCPサーバーの簡単な作り方でした。

特定のディレクトリ以下のファイルを返すMCPサーバー

AI Agent搭載エディターはファイル探してコンテキストに取り込んでくれたりしますよね?
ただ、探す時間が長かったり、検討違いしてたりするケースがままあります。
そこで、最初からほしいコードを返してくれるMCPサーバーがあればいいのでは?と思い、作ろうとしました。
ソースコードは以下に載せておきます。
指定したディレクトリのファイルを検索し、返してくれる処理を実装してます。 設定ファイルを外出しして、Goプログラムをいじらずにエンドポイントを作れるようにしました!

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log/slog"
    "os"
    "path/filepath"
    "strings"

    "github.com/bmatcuk/doublestar/v4"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

type QueryConfig struct {
    Name        string `json:"name"`         // クエリ名
    Description string `json:"description"`  // MCP説明
    Dir         string `json:"dir"`          // 環境変数ROOT_ENVからの相対パス
    PathPattern string `json:"path_pattern"` // パスパターン(glob)
}

func loadConfig(filename string) ([]QueryConfig, error) {
    file, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    var cfg []QueryConfig
    err = json.Unmarshal(file, &cfg)
    return cfg, err
}

func concatFilesWithGlob(root, pattern string) (string, error) {
    var builder strings.Builder
    matches, err := doublestar.Glob(os.DirFS(root), pattern)
    if err != nil {
        return "", err
    }

    for _, match := range matches {
        fullPath := filepath.Join(root, match)
        data, err := os.ReadFile(fullPath)
        if err != nil {
            continue
        }
        builder.WriteString(fmt.Sprintf("==== %s ====\n", match))
        builder.Write(data)
        builder.WriteString("\n\n")
    }

    return builder.String(), nil
}

func main() {
    // 環境変数取得
    rootDir := os.Getenv("ROOT_DIR")
    if rootDir == "" {
        panic("環境変数 ROOT_DIR が未設定です")
    }

    configPath := os.Getenv("CONFIG_PATH")
    if configPath == "" {
        slog.Error("CONFIG_PATHが未設定")
    }

    // MCPサーバー初期化
    s := server.NewMCPServer("Dynamic MCP Server", "1.0.0")

    // 設定ファイル読み込み
    configs, err := loadConfig(configPath)
    if err != nil {
        slog.Error(fmt.Errorf("設定ファイル読み込み失敗: %w", err).Error())
    }

    // 各ツールを登録
    for _, cfg := range configs {
        tool := mcp.NewTool(cfg.Name,
            mcp.WithDescription(cfg.Description),
        )

        localCfg := cfg // クロージャで固定

        // rootDir + cfg.Dir に解決(再帰探索のベース)
        fullSearchRoot := filepath.Join(rootDir, localCfg.Dir)

        s.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            result, err := concatFilesWithGlob(fullSearchRoot, localCfg.PathPattern)
            if err != nil {
                return mcp.NewToolResultError(fmt.Sprintf("ファイル探索エラー: %v", err)), nil
            }
            return mcp.NewToolResultText(result), nil
        })
    }

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

config.jsonは次のようになってます。
ROOT_DIRに対象にしたいプロジェクトのルートディレクトリのパスを書きます。
GONFIG_PATHにはクエリや走査対象のディレクトリを書きます
↓config.json

{
    "mcpServers": {
      "current-time": {
        "command": "./current-time-server",
        "args": [],
        "env": {
        }
      },
      "file-finder": {
        "command": "./mcp-file-finder",
        "args": [],
        "env": {
          "ROOT_DIR": "./test",
          "CONFIG_PATH": "./finder-config.json"
        }
      }
    }
}

GONFIG_PATHのほうにはエンドポイント名、LLMへの説明、走査対象ディレクトリ、マッチさせるファイルパターンを書きます。
↓finder-config.json

[
    {
      "name": "docs_query",
      "description": ".vscode以下の情報を取得します。vscodeなどの設定がほしい場合に使用してください",
      "dir": "./github-mcp-server/.vscode",
      "path_pattern": "**"
    },
    {
      "name": "e2e_code_query",
      "description": "ソースコードファイルを読み込みます。E2Eテストのコードです。",
      "dir": "./github-mcp-server/e2e",
      "path_pattern": "**/*.go"
    }
  ]
 

実際に叩いてみると以下のようにファイル内容を取得できます。

まとめ

今回はMCPサーバーを2種類作ってみました!
mcp-goは非常に簡単にMCPサーバーを作れ、Goバイナリにできるため、配布・使用が非常に簡単です。
2つ目のプログラムについては実用性も非常に高いと思います!
記事を見た皆さんもぜひ試してみてください!

Copyright © RAKUS Co., Ltd. All rights reserved.