Go実装のリモートMCPサーバーをAgentCore Runtimeにホスティングする

Go実装のリモートMCPサーバーをAgentCore Runtimeにホスティングする

2025.08.18

はじめに

Amazon Bedrock AgentCore Runtimeで Model Context Protocol (MCP) サーバーをデプロイできます。AWS公式からPythonのFastMCPを使った例が紹介されています。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-mcp.html

Goで試した経緯として、以下の記事でAWS Marketplace経由でAgentCore RuntimeにTerraform MCP Serverがホスティングできることを確認し、Go(シングルバイナリ)でもホスティング出来るんだなーと思い試してみました。

https://dev.classmethod.jp/articles/terraform-mcp-server-aws-marketplace-bedrock-agent-core/

基本terraform-mcp-serverの関連がありそうなmcp-goの設定を確認し、適用した形です。

https://github.com/hashicorp/terraform-mcp-server

以下のリソースを作成します。

  • ECR
  • Cognito
  • IAM
  • Bedrock AgentCore

ホスティングする

ソースコードは以下の通りです。

  • 8000ポートでListen
  • StreamableHTTPServerでステートレスオプションを設定
main.go
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"

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

var port = flag.String("port", "8000", "port to listen on")

func corsMiddleware(port string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", fmt.Sprintf("http://127.0.0.1:%s", port))
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization")
			w.Header().Set("Access-Control-Allow-Credentials", "true")

			if r.Method == "OPTIONS" {
				w.WriteHeader(http.StatusOK)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

func handleAddTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := request.GetArguments()

	a, aOk := args["a"].(float64)
	b, bOk := args["b"].(float64)

	if !aOk || !bOk {
		return &mcp.CallToolResult{
			Content: []mcp.Content{
				mcp.TextContent{
					Type: "text",
					Text: "Error: Invalid arguments. Both 'a' and 'b' must be numbers.",
				},
			},
		}, nil
	}

	result := int(a) + int(b)
	return &mcp.CallToolResult{
		Content: []mcp.Content{
			mcp.TextContent{
				Type: "text",
				Text: fmt.Sprintf("The sum of %d and %d is %d", int(a), int(b), result),
			},
		},
	}, nil
}

func main() {
	flag.Parse()

	mcpServer := server.NewMCPServer(
		"go-bin-mcp-server",
		"v1.0.0",
		server.WithToolCapabilities(true),
		server.WithLogging(),
	)

	mcpServer.AddTool(mcp.NewTool("add",
		mcp.WithDescription("Add two numbers"),
		mcp.WithNumber("a",
			mcp.Required(),
			mcp.Description("First number to add"),
		),
		mcp.WithNumber("b",
			mcp.Required(),
			mcp.Description("Second number to add"),
		),
	), handleAddTool)

	streamableServer := server.NewStreamableHTTPServer(mcpServer, server.WithStateLess(true))

	mux := http.NewServeMux()

	mux.Handle("/mcp", streamableServer)

	log.Printf("Server listening at http://localhost:%s", *port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", *port), corsMiddleware(*port)(mux)))
}

依存関係は以下の通りです。

go.mod
module github.com/shuntaka9576/agentcore-mcp-sample-go

go 1.25.0

require github.com/mark3labs/mcp-go v0.37.0

require (
	github.com/bahlo/generic-list-go v0.2.0 // indirect
	github.com/buger/jsonparser v1.1.1 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/invopop/jsonschema v0.13.0 // indirect
	github.com/mailru/easyjson v0.7.7 // indirect
	github.com/spf13/cast v1.7.1 // indirect
	github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

Dockerfileを書きます。ランタイム特有の特別なことはありません。

Dockerfile
FROM --platform=linux/arm64 golang:1.25-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY main.go ./

RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o mcp-server main.go

FROM --platform=linux/arm64 gcr.io/distroless/base-debian11

WORKDIR /app

COPY --from=builder /app/mcp-server .

ENTRYPOINT ["./mcp-server"]

ECRを作成します

$ export AWS_REGION=us-west-2

# TODO: AWS資格情報を取得

$ export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
$ export APP_NAME="go-bin-mcp-server"
$ aws ecr create-repository --repository-name $APP_NAME --region $AWS_REGION

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-west-2:<AWSアカウントID>:repository/go-bin-mcp-server",
        "registryId": "<AWSアカウントID>",
        "repositoryName": "go-bin-mcp-server",
        "repositoryUri": "<AWSアカウントID>.dkr.ecr.us-west-2.amazonaws.com/go-bin-mcp-server",
        "createdAt": "2025-08-18T12:52:05.171000+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

ビルドしてpushします

$ aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
$ docker buildx build --platform linux/arm64 -t "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/${APP_NAME}:latest" --push .

# pushされたことの確認
$ aws ecr describe-images --repository-name $APP_NAME --region $AWS_REGION

IAMを作成します。

長いので折りたたみ
cat > bedrock-agentcore-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRImageAccess",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer"
      ],
      "Resource": [
        "arn:aws:ecr:${AWS_REGION}:${AWS_ACCOUNT_ID}:repository/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:DescribeLogStreams",
        "logs:CreateLogGroup"
      ],
      "Resource": [
        "arn:aws:logs:${AWS_REGION}:${AWS_ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:DescribeLogGroups"
      ],
      "Resource": [
        "arn:aws:logs:${AWS_REGION}:${AWS_ACCOUNT_ID}:log-group:*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:${AWS_REGION}:${AWS_ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"
      ]
    },
    {
      "Sid": "ECRTokenAccess",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "xray:PutTraceSegments",
        "xray:PutTelemetryRecords",
        "xray:GetSamplingRules",
        "xray:GetSamplingTargets"
      ],
      "Resource": [
        "*"
      ]
    },
    {
      "Effect": "Allow",
      "Resource": "*",
      "Action": "cloudwatch:PutMetricData",
      "Condition": {
        "StringEquals": {
          "cloudwatch:namespace": "bedrock-agentcore"
        }
      }
    },
    {
      "Sid": "GetAgentAccessToken",
      "Effect": "Allow",
      "Action": [
        "bedrock-agentcore:GetWorkloadAccessToken",
        "bedrock-agentcore:GetWorkloadAccessTokenForJWT",
        "bedrock-agentcore:GetWorkloadAccessTokenForUserId"
      ],
      "Resource": [
        "arn:aws:bedrock-agentcore:${AWS_REGION}:${AWS_ACCOUNT_ID}:workload-identity-directory/default",
        "arn:aws:bedrock-agentcore:${AWS_REGION}:${AWS_ACCOUNT_ID}:workload-identity-directory/default/workload-identity/*"
      ]
    },
    {
      "Sid": "BedrockModelInvocation",
      "Effect": "Allow",
      "Action": [
        "bedrock:InvokeModel",
        "bedrock:InvokeModelWithResponseStream",
        "bedrock:ApplyGuardrail"
      ],
      "Resource": [
        "arn:aws:bedrock:*::foundation-model/*",
        "arn:aws:bedrock:${AWS_REGION}:${AWS_ACCOUNT_ID}:*"
      ]
    }
  ]
}
EOF

aws iam create-policy \
  --policy-name ECRBedrockAgentCorePolicy \
  --policy-document file://bedrock-agentcore-policy.json

cat > trust-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AssumeRolePolicy",
      "Effect": "Allow",
      "Principal": {
        "Service": "bedrock-agentcore.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "${AWS_ACCOUNT_ID}"
        },
        "ArnLike": {
          "aws:SourceArn": "arn:aws:bedrock-agentcore:${AWS_REGION}:${AWS_ACCOUNT_ID}:*"
        }
      }
    }
  ]
}
EOF

aws iam create-role \
  --role-name ECRBedrockAgentCoreRole \
  --assume-role-policy-document file://trust-policy.json \
  --description "Role for Bedrock Agent Core to access ECR"

aws iam attach-role-policy \
  --role-name ECRBedrockAgentCoreRole \
  --policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/ECRBedrockAgentCorePolicy"

congitoを作成します。

export POOL_ID=$(aws cognito-idp create-user-pool \
  --pool-name "MyUserPool" \
  --policies '{"PasswordPolicy":{"MinimumLength":8}}' \
  --region us-west-2 | jq -r '.UserPool.Id')

export CLIENT_ID=$(aws cognito-idp create-user-pool-client \
  --user-pool-id $POOL_ID \
  --client-name "MyClient" \
  --no-generate-secret \
  --explicit-auth-flows "ALLOW_USER_PASSWORD_AUTH" "ALLOW_REFRESH_TOKEN_AUTH" \
  --region us-west-2 | jq -r '.UserPoolClient.ClientId')

aws cognito-idp admin-create-user \
  --user-pool-id $POOL_ID \
  --username "testuser" \
  --temporary-password "TEMP_PASSWORD" \
  --region us-west-2 \
  --message-action SUPPRESS > /dev/null

aws cognito-idp admin-set-user-password \
  --user-pool-id $POOL_ID \
  --username "testuser" \
  --password "PERMANENT_PASSWORD" \
  --region us-west-2 \
  --permanent > /dev/null

Bedrock Agent coreを作成します。

CONTAINER_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${APP_NAME}:latest"
DISCOVERY_URL="https://cognito-idp.${AWS_REGION}.amazonaws.com/${POOL_ID}/.well-known/openid-configuration"

aws bedrock-agentcore-control create-agent-runtime \
  --region $AWS_REGION \
  --agent-runtime-name "go_bin_mcp_server" \
  --description "go_bin_mcp_server" \
  --agent-runtime-artifact "{
    \"containerConfiguration\": {
      \"containerUri\": \"${CONTAINER_URI}\"
    }
  }" \
  --role-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ECRBedrockAgentCoreRole" \
  --network-configuration '{
    "networkMode": "PUBLIC"
  }' \
  --protocol-configuration '{
    "serverProtocol": "MCP"
  }' \
  --authorizer-configuration "{
    \"customJWTAuthorizer\": {
      \"discoveryUrl\": \"${DISCOVERY_URL}\",
      \"allowedClients\": [\"${CLIENT_ID}\"]
    }
  }"

以下のような出力になるので、agentRuntimeArnをメモります。

結果
{
    "agentRuntimeArn": "arn:aws:bedrock-agentcore:us-west-2:<AWSアカウントID>:runtime/go_bin_mcp_server-lTJSgE5ZBg",
    "workloadIdentityDetails": {
        "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:<AWSアカウントID>:workload-identity-directory/default/workload-identity/go_bin_mcp_server-lTJSgE5ZBg"
    },
    "agentRuntimeId": "go_bin_mcp_server-lTJSgE5ZBg",
    "agentRuntimeVersion": "1",
    "createdAt": "2025-08-18T04:25:53.080291+00:00",
    "status": "CREATING"
}

マネコンからデプロイされていることを確認できます。

CleanShot 2025-08-18 at 14.44.45@2x

export AGENT_ARN="arn:aws:bedrock-agentcore:us-west-2:<AWSアカウントID>:workload-identity-directory/default/workload-identity/go_bin_mcp_server-lTJSgE5ZBg"
export BEARER_TOKEN=$(aws cognito-idp initiate-auth \
  --client-id "$CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters USERNAME='testuser',PASSWORD='PERMANENT_PASSWORD' \
  --region us-west-2 | jq -r '.AuthenticationResult.AccessToken')

# 確認
echo $BEARER_TOKEN

export ENCODED_ARN=$(echo "$AGENT_ARN" | jq -rR '@uri')
export MCP_URL="https://bedrock-agentcore.us-west-2.amazonaws.com/runtimes/${ENCODED_ARN}/invocations?qualifier=DEFAULT"

$ curl -X POST "$MCP_URL" \
  -H "authorization: Bearer $BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}}}, "id": 1}'

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"logging":{},"tools":{"listChanged":true}},"serverInfo":{"name":"go-bin-mcp-server","version":"v1.0.0"}}}

$ curl -X POST "$MCP_URL" \
  -H "authorization: Bearer $BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2}'

{"jsonrpc":"2.0","id":2,"result":{"tools":[{"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},"description":"Add two numbers","inputSchema":{"properties":{"a":{"description":"First number to add","type":"number"},"b":{"description":"Second number to add","type":"number"}},"required":["a","b"],"type":"object"},"name":"add"}]}}

Claude Codeに設定してみます。

jq -n \
  --arg token "$BEARER_TOKEN" \
  --arg arn "$AGENT_ARN" \
  '{
    go_bin_mcp_server: {
      headers: {
        Authorization: "Bearer \($token)"
      },
      type: "http",
      url: "https://bedrock-agentcore.us-west-2.amazonaws.com/runtimes/\($arn | @uri)/invocations?qualifier=DEFAULT"
    }
  }'
出力
{
  "go_bin_mcp_server": {
    "headers": {
      "Authorization": "Bearer eyJ..."
    },
    "type": "http",
    "url": "https://bedrock-agentcore.us-west-2.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-west-2%3A<AWSアカウントID>%3Aruntime%2Fgo_bin_mcp_server-lTJSgE5ZBg/invocations?qualifier=DEFAULT"
  }
}

出力されたMCP設定を導入します。

~/.claude.json
{
  mcpServers: {
    go_bin_mcp_server: {
      headers: {
        Authorization: 'Bearer eyJ...',
      },
      type: 'http',
      url: 'https://bedrock-agentcore.us-west-2.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-west-2%3A<AWSアカウントID>%3Aruntime%2Fgo_bin_mcp_server-lTJSgE5ZBg/invocations?qualifier=DEFAULT',
    },

(中略)

ツールが確認できました。

$ claude
╭───────────────────────────────────────────────────────────────────────────────╮
│ ✻ Welcome to Claude Code!                                                     │
│                                                                               │
│   /help for help, /status for your current setup                              │
│                                                                               │
│   cwd: /Users/shuntaka/repos/github.com/shuntaka9576/agentcore-mcp-sample-go  │
╰───────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Tools for go_bin_mcp_server (1 tools)                                                                                                                                                               │
│                                                                                                                                                                                                     │
│ ❯ 1. add  destructive, open-world                                                                                                                                                                   │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
   Esc to go back

呼び出しも確認できました。

$ claude
╭───────────────────────────────────────────────────────────────────────────────╮
│ ✻ Welcome to Claude Code!                                                     │
│                                                                               │
│   /help for help, /status for your current setup                              │
│                                                                               │
│   cwd: /Users/shuntaka/repos/github.com/shuntaka9576/agentcore-mcp-sample-go  │
╰───────────────────────────────────────────────────────────────────────────────╯

> mcpを使って3+28をしてください

⏺ MCPを使って計算をします。

  go_bin_mcp_server - add (MCP)(a: 3, b: 28)
  ⎿  The sum of 3 and 28 is 3131

さいごに

最初はgo-sdkで試していたのですが、StreambleHTTPのセッションなし設定がよくわからずterraform-mcp-serverの実装を確認してこちらの実装に落ち着きました。シングルバイナリで動作しているので他の言語(nodejs, deno, Rust...)でも可能そうですね!サーバーレスかつAWS上でサクッとリモートMCPをたてられるのは便利でいいですね!認証部分(Authorizationヘッダーをつける形にはなりますが...)をマネージドで処理してくれる点も良さそうです!

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.