🤖

「おっす」で始める!Goの公式MCP SDK × LangChainGoでエージェント実装

に公開

始めに

LLMやAIの開発はほとんどPythonで行われているかと思いますが、Goのエコシステムにも素敵なフレームワークがあるので、サクッと使い方を紹介したいと思います。

チャットボットなら200行以下で作れちゃいます!

必要な環境

MCPサーバーの作り方

まずは独立したMCPサーバーの作成から始めます。
現時点でのMCPサーバーSDKはいくつかありますが、主として以下の3つが存在します。

その中で最も多く使われているのは mark3labs/mcp-go でしょう。ただ、Goのメンテナーが開発している modelcontextprotocol/go-sdk の方が、そのうち一般的になると思いますので、早めに乗り換えたほうが良いでしょう。(注:リリース前のため、APIが若干変更される可能性があります)

挨拶をするMCPサーバー

package greeter

import (
	"context"
	"fmt"
	"log"

	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/tmc/langchaingo/tools"
)

const (
	ToolName = "greet"
	ToolDesc = "挨拶をする"
)

type GreetParams struct {
	Name string `json:"name" jsonschema:"挨拶する人の名前"`
}

func Greet(
	ctx context.Context,
	cc *mcp.ServerSession,
	params *mcp.CallToolParamsFor[GreetParams],
) (*mcp.CallToolResultFor[any], error) {
	return &mcp.CallToolResultFor[any]{
		Content: []mcp.Content{
			&mcp.TextContent{
				Text: "おっす" + params.Arguments.Name + "、元気かい?",
			},
		},
	}, nil
}

これでロジックは完成です。Greet はハンドラー関数になっていて、*mcp.CallToolParamsFor を受け取って、*mcp.CallToolResultFor を返却する設計になっています。

Content は配列になっているので、複数のコンテンツを返すことも可能です。また、コンテンツタイプとして TextContent, AudioContentImageContent があるので、画像生成やTTSなども簡単に実装できるようになっています。

呼び出し

HTTPサーバーと同じようなイメージで、ハンドラー関数をサーバーにつなげて実行する必要があります。

package main

import (
	"context"
	"demo/mcp/greeter"
	"log"

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

func main() {
	server := mcp.NewServer(&mcp.Implementation{
		Name: "greeter", Version: "v1.0.0",
	}, nil)

	mcp.AddTool(server, &mcp.Tool{
		Name:        greeter.ToolName,
		Description: greeter.ToolDesc,
	}, greeter.Greet)

	ctx := context.Background()
	err := server.Run(ctx, mcp.NewStdioTransport())
	if err != nil {
		log.Fatal(err)
	}
}

これでサーバーも完成です。
サーバーの接続方法はいくつかありますが、今回はローカルで実行するため、standard I/O を使います。

ツールを使ってみる

上記の main.go ファイルを、まずはbuildします。

go build ./cmd/greeter-mcp

Clineに登録するために 赤い丸で示したサーバーラックのような見た目のアイコンを選択し、「Installed」タブに移動してから、「Advanced MCP Settings」をクリックします。

開いたJSONファイルに以下の設定を追加します。

{
  "mcpServers": {
    "greeter": {
      "autoApprove": [],
      "disabled": false,
      "timeout": 60,
      "type": "stdio",
      "command": "/demo/mcp-server",
      "args": []
    }
  }
}

ここでの command は先程コンパイルしたバイナリファイルを示しています。
typeserver.Run で設定したトランスポートの種類ですね(mcp.NewStdioTransport())。

JSONファイルを保存すると、以下のように表示されるはずです。

最後に「Done」で設定を適用すると Cline が greeter ツールを使えるようになっているはずです。

確認してみましょう。

ちゃんと出ていますね。

動作も問題なさそうです。

LangChainGo との連携

次は先ほど作ったMCPサーバーをエージェントと連携します。

設計

イメージとして上記のような作りになります。

  1. エージェントがプロンプトを受け取り、LLMに渡す
  2. LLMが(プロンプトによって)ツールを呼び出したいとエージェントに伝える
  3. エージェントがMCPクライアントを使って、MCPサーバーにあるツールハンドラーを呼び出す
  4. エージェントがそのハンドラーのレスポンスを受け取り、LLMに渡す
  5. LLMがツールのレスポンスを使って文章を生成する

.env ファイル

今回はBedrockを使うため、AWSの設定は環境変数から設定します。

AWS_DEFAULT_PROFILE=******
AWS_REGION=us-east-1

そして、環境変数を適用しましょう。

package main

// 省略

func init() {
	if err := godotenv.Load(".env"); err != nil {
		log.Fatal("error loading .env file")
	}
}

エージェントの仕組み

まずはモデルを指定します。

func main() {
	model := "us.anthropic.claude-sonnet-4-20250514-v1:0"
	ctx := context.Background()

モデルIDの頭にある us. はcross-region inferenceのためのプレフィックスです。Bedrockの設定によってこのプレフィックスをつけておかないと認識されない可能性があります。

次は先程と同じようにMCPサーバーを実行します。

	server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v0.0.1"}, nil)
	mcp.AddTool(server, &mcp.Tool{Name: greeter.ToolName, Description: greeter.ToolDesc}, greeter.Greet)
	serverTransport, clientTransport := mcp.NewInMemoryTransports()

	// サーバーを実行して、エラーを待つ
	onServerExit := make(chan error)
	go func() {
		onServerExit <- server.Run(ctx, serverTransport)
	}()

そして今回はMCPクライアントも必要です。そのため、mcp.NewInMemoryTransportsを呼び出しています。これは、サーバーとクライアントを接続するために必要です。

	client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
	session, err := client.Connect(ctx, clientTransport)
	if err != nil {
		log.Fatal("connect client: ", err)
	}

以降は tmc/langchaingo の設定です。まずはバックエンドとモデルを指定します。

	llm, err := bedrock.New(
		bedrock.WithModel(model),
	)
	if err != nil {
		log.Fatal("new bedrock:", err)
	}

次は必須ではありませんが、エージェントにチャット履歴を渡すことができるので、試しにこれも設定してみましょう。

	chatMemory := memory.NewConversationBuffer()
	err = chatMemory.ChatHistory.AddUserMessage(ctx, "田中です")
	if err != nil {
		log.Fatal("add user message to memory: ", err)
	}

ちなみにLLMのメッセージも設定可能です。

続けて、エージェントの設定です。

	agent := agents.NewConversationalAgent(
		llm,
		[]tools.Tool{
			// TODO
		},
		agents.WithMemory(chatMemory),
		agents.WithPromptPrefix(
			"丁寧語を絶対に使わないで",
		),
	)

ここでMCPサーバーのツールを設定することができますが、tmc/langchaingo はまだ go-sdk/mcp のツールをそのまま利用できないため、少し変換を行う必要があります。

ツールの変換

tools.Tool は、実際には以下のようなインターフェイスです。

type Tool interface {
    Name() string
    Description() string
    Call(ctx context.Context, input string) (string, error)
}

つまり、tmc/langchaingo ではツールのインプットとアウトプットは string になっています。
最初に作成した greeter パッケージに戻って、変換用の関数を追加しましょう。

package greeter

// 省略

func AsLangchaingoTool(sess *mcp.ClientSession) tools.Tool {
	return &langchaingoTool{
		sess: sess,
	}
}

type langchaingoTool struct {
	sess *mcp.ClientSession
}

go-sdk/mcp の設計上ではツールの呼び出しはクライアントセッションから行うようになっています。

まずは Name()Description() です。

func (t *langchaingoTool) Name() string {
	return ToolName
}

func (t *langchaingoTool) Description() string {
	return ToolDesc
}

Call() は以下のようになります。

func (t *langchaingoTool) Call(ctx context.Context, input string) (string, error) {
	log.Println("calling tool:", input)

	params := GreetParams{
		Name: input,
	}
	res, err := t.sess.CallTool(ctx, &mcp.CallToolParams{
		Name:      ToolName,
		Arguments: params,
	})
	if err != nil {
		return "", fmt.Errorf("call tool: %w", err)
	}
	if res.IsError {
		log.Print("tool failed")
	}

	var result string
	for _, content := range res.Content {
		switch c := content.(type) {
		case *mcp.TextContent:
			if result != "" {
				result += "\n"
			}
			result += c.Text
		default:
			log.Println("other content type:", c)
		}
	}
	log.Println("tool call result:", result)
	return result, nil
}

少し長くなっていますが、やっていることはとても単純です。
まずはツールを呼び出すための params を生成し、実際にツールを呼び出します。

エラーが発生しなければレスポンスを受け取って返します。
レスポンスのタイプは3つもあるため、タイプアサートが必要ですが、今回のツール作成者は私たちなので、Text 以外は来ないことがわかっていますね。

MCPサーバーの仕様としてはレスポンスが複数来る可能性があるので、そのまま連結させて返却しています。

これでツールの型変換も完成です。

エージェントの続き

先程のコードに変換を加えると以下のようになります。

	agent := agents.NewConversationalAgent(
		llm,
		[]tools.Tool{
			greeter.AsLangchaingoTool(session),
		},
		agents.WithMemory(chatMemory),
		agents.WithPromptPrefix(
			"丁寧語を絶対に使わないで",
		),
	)

そして最後に、エージェントを実行するコードです。今回はチャットボット風に作りたいので、コンソールから会話できるようにしたいと思います。

	exec := agents.NewExecutor(
		agent,
		agents.WithMemory(chatMemory),
	)

	reader := bufio.NewReader(os.Stdin)
	for {
		text, err := reader.ReadString('\n')
		if err != nil {
			log.Fatal("read stdin: ", err)
		}
		res, err := chains.Run(ctx,
			exec,
			text,
		)
		if err != nil {
			log.Fatal("run chain: ", err)
		}
		fmt.Println("LLM:", res)
	}

お気づきの方もいるかもしれませんが、agent でも exec でも chatMemory を設定しています。こちらはおそらく片方でも問題ないはずですが、両側に設定しても害はないので、両側で設定しています。

エージェントの実行は chains.Run で行うことができます。そこに実際のプロンプトを渡し、LLMからのレスポンスを受け取ることができます。

テスト

テストをしてみました。

良い感じにツールを使ったエージェントが出来上がったのではないでしょうか!

~/git/demo$ go run ./cmd/chat
こんにちは
2025/08/13 19:05:18 calling tool: 田中さんへの挨拶
2025/08/13 19:05:18 tool call result: おっす田中さんへの挨拶、元気かい?
LLM:  おっす田中!元気?

見ての通り、ツールについた説明だけではエージェントが微妙に間違った使い方をしてしまう可能性があるため、システムプロンプトやプロンプトプレフィックスで調整しても良いかもしれません。

main.go の全体はこちらです。
package main

import (
	"bufio"
	"context"
	"demo/mcp/greeter"
	"fmt"
	"log"
	"os"

	"github.com/joho/godotenv"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/tmc/langchaingo/agents"
	"github.com/tmc/langchaingo/chains"
	"github.com/tmc/langchaingo/llms/bedrock"
	"github.com/tmc/langchaingo/memory"
	"github.com/tmc/langchaingo/tools"
)

func init() {
	if err := godotenv.Load(".env"); err != nil {
		log.Fatal("Error loading .env file")
	}
}

func main() {
	model := "us.anthropic.claude-sonnet-4-20250514-v1:0"
	ctx := context.Background()

	server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v0.0.1"}, nil)
	mcp.AddTool(server, &mcp.Tool{Name: greeter.ToolName, Description: greeter.ToolDesc}, greeter.Greet)
	serverTransport, clientTransport := mcp.NewInMemoryTransports()

	onServerExit := make(chan error)
	go func() {
		onServerExit <- server.Run(ctx, serverTransport)
	}()

	client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
	session, err := client.Connect(ctx, clientTransport)
	if err != nil {
		log.Fatal("connect client: ", err)
	}

	llm, err := bedrock.New(
		bedrock.WithModel(model),
	)
	if err != nil {
		log.Fatal("new bedrock:", err)
	}

	chatMemory := memory.NewConversationBuffer()
	err = chatMemory.ChatHistory.AddUserMessage(ctx, "田中です")
	if err != nil {
		log.Fatal("add user message to memory: ", err)
	}
	agent := agents.NewConversationalAgent(
		llm,
		[]tools.Tool{
			greeter.AsLangchaingoTool(session),
		},
		agents.WithMemory(chatMemory),
		agents.WithPromptPrefix(
			"丁寧語を絶対に使わないで",
		),
	)

	exec := agents.NewExecutor(
		agent,
		agents.WithMemory(chatMemory),
	)

	reader := bufio.NewReader(os.Stdin)
	for {
		text, err := reader.ReadString('\n')
		if err != nil {
			log.Fatal("read stdin: ", err)
		}
		res, err := chains.Run(ctx,
			exec,
			text,
		)
		if err != nil {
			log.Fatal("run chain: ", err)
		}
		fmt.Println("LLM:", res)
	}
}

終わりに

今回は、これまでほとんどPythonで行われてきたLLMやAIの開発を、Goでも実現できるようなサンプルを作ってみました。

複雑なエージェントを作ろうとした場合、Goではまだ不十分なところがあるかもしれませんが、今後、SDKなどの成長も大いに期待できると思います。

ちなみに、エージェントに渡したプロンプトプレフィックスを外したらどうなると思いますか?

興味のある方は是非、試していただき、どのような結果になったのかコメントしてください。

Finatext Tech Blog

Discussion