every Tech Blog

株式会社エブリーのTech Blogです。

【ハンズオン】 MCP サーバー作成からリモートにホスティングしてみる

はじめに

こんにちは、@きょーです!普段はデリッシュキッチン開発部のバックエンド中心で業務をしています。

このブログでは簡単な MCP サーバーを作成し、ローカルでの動作確認。そしてリモート化させるところまでをハンズオン形式で紹介しようと思います。すでに MCP サーバーを多数作成されていたり、豊富な知見をお持ちの方には物足りない内容になっているかもしれません。

MCP サーバーとは

MCP サーバーは、AI アプリケーションと外部システムの間の橋渡しをする役割を担います。具体的には以下のような機能を提供します。

  • リソース
    • ファイルやデータベースなどの外部リソースへのアクセス
  • ツール
    • 外部 API の呼び出しや特定の操作の実行

自分がよく使っている MCP サーバーを例としてあげると GitHub があり、主に issue の読み込み、作成や PR の作成などをしてもらっています。今となっては手放せない MCP サーバーです。

他にも自分や他の人が使っている MCP サーバーとして以下のようなものもあります。

github.com (↑ 最近見つけた面白いリポジトリがあるので共有させてください。いろんな MCP サーバーが紹介されています。)

MCP の詳細な説明は公式に書かれているためここでは説明を省略とさせてください。

ハンズオン

このハンズオンではmcp/go-sdkの実装をもとに自分の名前を入力したら「Hi, {自分の名前}」と返す MCP サーバーを作成します。最初はローカル環境のみで動作できるようにサーバーを構築し、その後リモート環境(cloud run)に載せられるようにサーバーを修正していきます。完成したコードはこちらのリポジトリに残してあるので、適宜見に行っていただけますと幸いです。

必要な環境は以下の通りです。

  • golang
  • docker
  • google cloud にログインできるアカウント
  • node.js: ^22.7.5 (動作確認で使うツール用)

それでは実際にハンズオン形式でやっていこうと思います。

step 1

まずはローカルで動く MCP サーバーを作成します(通信形式は STDIO)

# まずは作業場所を作成します
mkdir mcp-sample
cd mcp-sample

# 次にgo周りの環境を整えます
go mod init
touch main.go

# dockerファイルも用意しておきます
touch Dockerfile

ベースとなるコードをmain.goに書いていきます。公式のコードをそのまま持ってきます。

package main

import (
    "context"
    "log"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

type Input struct {
    Name string `json:"name" jsonschema:"the name of the person to greet"` // ユーザーに入力してもらうパラメータ
}

type Output struct {
    Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"` // アウトプットとなるデータの説明
}

// MCPサーバーに登録するツールの中身
func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (*mcp.CallToolResult, Output, error) {
    return nil, Output{Greeting: "Hi " + input.Name}, nil
}

func main() {
    // MCPサーバーを作成
    server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)

  // MCPサーバーにツールを登録
    mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)

  // サーバーを起動し、クライアントが接続を切るまで待機(通信方式はSTDIO)
    if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
        log.Fatal(err)
    }
}
FROM golang:1.24-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o mcp-greeter .

FROM gcr.io/distroless/base-debian12

WORKDIR /app
COPY --from=builder /app/mcp-greeter /app/mcp-greeter

ENTRYPOINT ["/app/mcp-greeter"]

ここでgo mod tidyを実行するとディレクトリ構成は以下のようになっているかと思います。

.
├── Dockerfile
├── go.mod
├── go.sum
└── main.go

以下のコマンドを実行してイメージを用意しておきます。

# イメージをビルド
docker build -t mcp-greeter .

mcp/inspectorというツールを使って動作確認をしてみます。

npx @modelcontextprotocol/inspector

上記のコマンドを実行し、inspector の画面で以下の情報を入力し画面下部にあるconnectをタップすると MCP サーバーのツールの動作確認などができるようになります。

  • Transport Type
    • STDIO
  • Command
    • docker
  • Arguments
    • run -i --rm mcp-greeter

これでローカル環境で MCP サーバーを作成、動作確認までは終えました。

step 2

次は MCP サーバーの通信方式を変えます。

STDIO は「クライアントが MCP サーバーをサブプロセスとして起動し、標準入出力で直接通信する」ことを前提にしています。これは同一マシン上でのプロセス間通信には適していますが、リモート環境では以下の問題があります。

  • クライアントがリモートサーバー上でサブプロセスを起動できない
  • 標準入出力による直接通信がネットワーク越しでは成立しない

そこで、MCP 仕様で定義されている Streamable HTTP の通信方式を使用することで、ネットワーク越しの通信を可能にさせます。これによって docker や golang などを必要としていた個人の環境に依存することなく、MCP クライアントさえあれば簡単に MCP サーバーを利用できるようになります。

STDIO から Streamable HTTP の通信方式に変えるにあたり MCP サーバーと MCP クライアントの通信は大きく変わります。

左: STDIO の通信方式  右: Streamable HTTP の通信方式

詳細は公式に書いてあるため省きますが、通信のやり取りから Streamable HTTP ではセッションの管理で大変そうなのがわかるかと思います。

これをコードに落とし込むためにmain.goを修正していきます。(修正した際の PR:https://github.com/keyl0ve/mcp-migration-sample/pull/1

func main() {
    // MCPサーバーを作成
    server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)

    // MCPサーバーにツールを登録
    mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)

-  // サーバーを起動し、クライアントが接続を切るまで待機(通信方式はSTDIO)
-  if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
-      log.Fatal(err)
-  }
+   // HTTPハンドラーを介してMCPリクエストを処理
+   handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
+       return server
+   }, nil)
+
+   // 8080でサーバーを起動
+   if err := http.ListenAndServe(":8080", handler); err != nil {
+       log.Fatal(err)
+   }
}

実際に動くか確認してみましょう。

# イメージのリビルド
docker build -t mcp-greeter .

# 8080でリクエストを受け付ける
docker run --rm -p 8080:8080 mcp-greeter

上記でサーバーを建て直したら inspector の設定を変えます。

無事に動いているのが確認できるかと思います。

ログを出すミドルウェアを挟む(対応 PR)と MCP サーバー側でどんなリクエストが来ているか確認することもできます。

% docker run --rm -p 8080:8080 mcp-greeter
2025/11/12 07:18:56 MCP HTTP server listening on :8080
2025/11/12 07:19:02 POST / from 192.168.65.1:49815 -> 200 (2.46225ms)
time=2025-11-12T07:19:02.807Z level=INFO msg="MCP method started" method=initialize session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:02.807Z level=INFO msg="MCP method completed" method=initialize session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
time=2025-11-12T07:19:02.821Z level=INFO msg="MCP method started" method=notifications/initialized session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:02.821Z level=INFO msg="MCP method completed" method=notifications/initialized session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=false
2025/11/12 07:19:02 POST / from 192.168.65.1:49815 -> 202 (64.833µs)
2025/11/12 07:19:02 POST / from 192.168.65.1:62785 -> 200 (436.167µs)
time=2025-11-12T07:19:02.825Z level=INFO msg="MCP method started" method=logging/setLevel session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:02.825Z level=INFO msg="MCP method completed" method=logging/setLevel session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
time=2025-11-12T07:19:07.384Z level=INFO msg="MCP method started" method=tools/list session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:07.385Z level=INFO msg="MCP method completed" method=tools/list session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
2025/11/12 07:19:07 POST / from 192.168.65.1:62785 -> 200 (1.72925ms)
time=2025-11-12T07:19:09.673Z level=INFO msg="MCP method started" method=tools/call session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:09.673Z level=INFO msg="Calling tool" name=greet args="{\"name\":\"aaa\"}"
time=2025-11-12T07:19:09.673Z level=INFO msg="MCP method completed" method=tools/call session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
time=2025-11-12T07:19:09.673Z level=INFO msg="tool result" isError=false structuredContent="{\"greeting\":\"Hi aaa\"}"
2025/11/12 07:19:09 POST / from 192.168.65.1:62785 -> 200 (1.143916ms)

step 3

サーバーをホスティングしていきます。

色々な方法でホスティングすることはできますが、今回はドキュメントも豊富だった cloud run を使っていこうと思います。試してはいませんが、aws の ecs などでもホスティングはできるかと思います。

# ログイン
gcloud auth login

# プロジェクトの選択(自分が所属しているプロジェクトを選択してください)
gcloud config set project {sample-project}

# イメージの作成
docker build --platform linux/amd64 -t gcr.io/{sample-project}/mcp-greeter:latest .

# イメージのアップロード
docker push gcr.io/{sample-project}/mcp-greeter:latest

# cloud runにデプロイ(値は適当です)
gcloud run deploy mcp-greeter --image gcr.io/{sample-project}/mcp-greeter:latest --region asia-northeast1 --project {sample-project} --platform managed --no-allow-unauthenticated --memory 512Mi --max-instances 3

cloud run にデプロイするときに--no-allow-unauthenticatedフラグをつけることで認証を強制させることができます。MCP サーバーに認証機能をつけたいけど、まだ実装できていない時につけると良さそうです。

先ほどのデプロイ時にはフラグをつけているのでそのままだと動作確認できないはずです。デプロイ時に吐き出された URL を inspector に貼り接続を確認してみてください。

gcloud auth print-identity-tokenコマンドを実行して token を取得しましょう。取得した token を inspector の Authorization に埋め込むことでリモート MCP サーバーに接続することができます。

(mcp.json に貼る場合は以下の通り)

  {
    "mcp-greeter": {
      "url": "https://sample-url",
      "headers": {
        "Authorization": "Bearer xxx"
      }
    }
  }

これでリモート MCP サーバーの作成は終了です。適宜ソースコードの内容を変えたり、ホスティングの方式を変えたりしてユースケースにあったリモート MCP サーバーを作成していけるかと思います。

以上です!お疲れ様でした!

最後に

実際にリモート MCP 化してみて、mcp/go-sdk は通信方式が変わってもアプリケーションコードへの影響を最小限に抑え、簡単にツールを拡張できるよう設計されていることがわかりました。また簡単に MCP サーバーをリモート化できるということを学べたので業務で活用できそうなケースがあればどんどん MCP サーバーを作っていきたいなと思います。

ただ、シンプルな MCP サーバーなら簡単にデプロイできそうだなと思いつつ、まだ以下のような課題はあると思っています。

  • 認証まわり
    • 今だと gcloud に権限がある人しかアクセスできない
    • oauth などで認証を突破させたい
  • 機密情報まわり
    • API Key を使ってアクセスしたいリソースがある場合、人によって変わる複数の API Key をどうやって管理すれば良いか

こういったケースのことはまだ考えられていないので引き続き色々と試してユースケースに合わせた MCP サーバーを構成できるようにしていきたいです!

もしいい感じに課題を解決できそうな方がいましたら気軽に ↓ の repo に PR や issue で教えていただけますと幸いです! github.com