用 LangChainGo 调用 MCP 工具:从零到一的工程实践指南
这篇文章面向 Go 语言开发者,手把手带你用 LangChainGo 实现大语言模型(LLM)调用 MCP(Model Context Protocol)工具的完整流程。我们将以开源项目 “langchaingo-mcp-adapter” 为例,演示如何把 MCP 工具变成 LangChainGo 的 Tool,并由 Agent 驱动调用。
阅读完,你将掌握:
- MCP 的工具机制与 LangChainGo 的工具接口如何融合
- 如何连接本地或远端 MCP 服务端并拉取工具
- 如何在 LangChainGo Agent 中使用这些工具完成智能体推理
- 项目结构、关键代码、运行示例与常见问题排查
目标受众:具备基础 Go 语言与 LLM 应用集成经验的开发者
什么是 MCP 与 LangChainGo?
- MCP(Model Context Protocol):一种通用协议,定义“工具(tool)”的发现、参数模式与调用返回,帮助 LLM 安全、标准化地调用外部功能与数据源。
- LangChainGo:LangChain 的 Go 语言实现,提供 LLM、链(Chain)、智能体(Agent)、工具(Tool)等组件,用于构建具备工具使用能力的 AI 应用。
结合 MCP 与 LangChainGo,你的 LLM 不再只是“对话”,而是可以调用标准化的工具,如检索数据、调用 API、执行任务,从而实现可控、可扩展的智能体系统。
项目概览与结构
示例仓库结构如下:
langchaingo-mcp-adapter/
├── README.md
├── README.zh-CN.md
├── adapter.go # 适配器核心:将 MCP Tool 转换为 LangChainGo Tool
├── example/
│ ├── agent/
│ │ └── main.go # Agent 示例:连接 MCP 并调用 Tool
│ └── server/
│ └── main.go # MCP Server 示例:定义一个 greet 工具
├── go.mod
└── go.sum
adapter.go:本项目的核心,提供一个Adapter,用于:- 连接到 MCP 服务端会话
- 调用
ListTools发现工具 - 把 MCP 的
Tool转为 LangChainGo 的tools.Tool,并实现Name、Description、Call
example/server/main.go:最小 MCP 服务端示例,注册一个greet工具,接受name返回问候语example/agent/main.go:演示如何连接服务端、拉取工具并在 LangChainGo Agent 中调用
适配器设计与关键实现
适配器的核心在于“接口转换”:MCP 的工具模型要符合 LangChainGo 的 tools.Tool 接口规范。
1. Adapter 结构与创建
type Adapter struct {
session *mcp.ClientSession // MCP 客户端会话
timeout time.Duration // 工具调用超时时间
}
func New(session *mcp.ClientSession, opts... Option) *Adapter {
adapter := &Adapter{
session: session,
timeout: 30 * time.Second,
}
for _, opt := range opts { opt(adapter) }
return adapter
}
session:与 MCP 服务端的连接会话timeout:控制调用工具的超时,避免阻塞
2. 获取MCP工具并转换为 LangChainGo Tool
func (a *Adapter) Tools(ctx context.Context) ([]tools.Tool, error) {
mcpTools, err := a.session.ListTools(ctx, &mcp.ListToolsParams{})
if err != nil { return nil, fmt.Errorf("ListTools error: %w", err) }
var langchainTools []tools.Tool
for _, tool := range mcpTools.Tools {
langchainTools = append(langchainTools, &mcpTool{
mcpTool: tool,
session: a.session,
timeout: a.timeout,
})
}
return langchainTools, nil
}
- 调用 MCP 的
ListTools获取工具并封装为内部的mcpTool
3. 实现 LangChainGo 的 tools.Tool 接口
type mcpTool struct {
mcpTool *mcp.Tool
session *mcp.ClientSession
timeout time.Duration
}
func (t *mcpTool) Name() string {
return t.mcpTool.Name
}
func (t *mcpTool) Description() string {
description := t.mcpTool.Description
if t.mcpTool.InputSchema != nil {
schema, _ := toString(t.mcpTool.InputSchema)
description += "\n The input schema is: " + schema
}
return description
}
func (t *mcpTool) Call(ctx context.Context, input string) (string, error) {
var args map[string]any
if err := json.Unmarshal([]byte(input), &args); err != nil {
return "", fmt.Errorf("invalid JSON input: %w", err)
}
result, err := t.session.CallTool(ctx, &mcp.CallToolParams{
Name: t.mcpTool.Name,
Arguments: args,
})
if err != nil { return "", fmt.Errorf("CallTool MCP tool '%s' error: %w", t.mcpTool.Name, err) }
if result == nil || result.Content == nil {
return "", fmt.Errorf("MCP tool '%s' returned nil result", t.mcpTool.Name)
}
var parts []string
for _, c := range result.Content {
if tc, ok := c.(*mcp.TextContent); ok { parts = append(parts, tc.Text) }
}
return strings.Join(parts, "\n"), nil
}
Description():拼接输入模式(InputSchema),提升可用性Call():以 JSON 字符串作为入参,解码为Arguments调用 MCP 工具,支持超时控制与返回拼接
搭建最小 MCP 服务端
示例服务端注册一个 greet 工具,接受 name 并返回问候语:
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"`
}
func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (
*mcp.CallToolResult, Output, error,
) {
return nil, Output{Greeting: "[MCP] Hello " + input.Name}, nil
}
func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
- 通过
jsonschema标签为输入/输出提供模式信息(便于可视化与验证) - 使用
StdioTransport在 stdin/stdout 上运行,便于本地开发测试
在 Agent 中使用 MCP 工具
下面演示如何启动/连接本地MCP 服务端,并在 LangChainGo Agent 中调用本地MCP 工具:
func connectLocalMCPServer(ctx context.Context) (*mcp.ClientSession, error) {
serverDir := "../server"
executableName := "server"
serverBinary := filepath.Join(serverDir, executableName)
// 编译示例服务端
if _, err := os.Stat(serverBinary); os.IsNotExist(err) {
cmd := exec.Command("go", "build", "-o", executableName, "main.go")
cmd.Dir = serverDir
if output, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("build server: %w\nOutput: %s", err, output)
}
}
// 连接本地的mcp server
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
transport := &mcp.CommandTransport{Command: exec.Command(serverBinary)}
session, err := client.Connect(ctx, transport, nil)
if err != nil { return nil, fmt.Errorf("connect to mcp server: %w", err) }
return session, nil
}
func main() {
godotenv.Load()
ctx := context.Background()
// 初始化 LLM(OpenAI 例子)
llm, err := openai.New(
openai.WithToken(os.Getenv("OPENAI_API_KEY")),
openai.WithBaseURL(os.Getenv("OPENAI_API_BASE_URL")),
openai.WithModel(os.Getenv("OPENAI_MODEL")),
)
if err != nil { panic(err) }
// 连接 MCP 示例服务端
session, err := connectLocalMCPServer(ctx)
if err != nil { log.Fatal(err) }
defer session.Close()
// 适配 MCP 工具为 LangChainGo Tool
adapter := mcpadapter.New(session, mcpadapter.WithToolTimeout(30*time.Second))
mcpTools, err := adapter.Tools(ctx)
if err != nil { log.Fatal(err) }
name := "leon"
payload, _ := json.Marshal(map[string]string{"name": name})
// 创建 Agent
agent := agents.NewOneShotAgent(llm, mcpTools, agents.WithMaxIterations(5))
executor := agents.NewExecutor(agent)
// Agent 是否调用工具,取决于 LLM 的决策。为了更明确的让它使用工具,把问题换成明确的指令
prompt := fmt.Sprintf("必须调用工具 greet,并传入参数 %s。完成后以 Final Answer: 为前缀原样返回工具输出,不要添加任何其他内容。", string(payload))
result, err := chains.Run(ctx, executor, prompt)
if err != nil { log.Fatalf("Agent execution error: %v", err) }
log.Println("agent result:", result)
}
- 本地示例使用
CommandTransport启动并连接 MCP 服务端 adapter.Tools()将 MCP 工具转换为 LangChainGo 的Tool,传入 Agentchains.Run()执行智能体流程,工具调用将由 Agent 根据 LLM 决策触发
连接远端 MCP(HTTP 流式)
如果 MCP 服务端提供 HTTP 流式接口(例如高德MCP服务),可以改用:
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
transport := &mcp.StreamableClientTransport{Endpoint: "https://mcp.example.com/mcp?key=YOUR_KEY"}
session, err := client.Connect(ctx, transport, nil)
- 适合部署在云端的 MCP 服务场景(如地图、天气、企业内部数据服务)
- 超时控制与工具模型一致,Agent 的调用逻辑无需变更
同时连接本地和远端MCP(HTTP 流式)
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"time"
"runtime"
"github.com/joho/godotenv"
mcpadapter "github.com/leondevpt/langchaingo-mcp-adapter"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/tmc/langchaingo/agents"
"github.com/tmc/langchaingo/chains"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/openai"
)
func main() {
godotenv.Load()
ctx := context.Background()
llm, err := initLLM()
if err != nil {
panic(err)
}
// 连接本地MCP服务器获取工具列表
session, err := connectLocalMCPServer(context.Background())
if err != nil {
log.Fatal(err)
}
defer session.Close()
adapter := mcpadapter.New(session, mcpadapter.WithToolTimeout(30*time.Second))
localMcpTools, err := adapter.Tools(ctx)
if err != nil {
log.Fatal(err)
}
// 连接高德地图MCP服务器获取工具列表
amapSession, err := connectAmapMCPServer(ctx)
if err != nil {
log.Fatal(err)
}
defer amapSession.Close()
amapAdapter := mcpadapter.New(amapSession, mcpadapter.WithToolTimeout(30*time.Second))
amapTools, err := amapAdapter.Tools(ctx)
if err != nil {
log.Fatal(err)
}
tools := append(localMcpTools, amapTools...)
// Create a agent with the tools
agent := agents.NewOneShotAgent(
llm,
tools,
agents.WithMaxIterations(5),
)
executor := agents.NewExecutor(agent)
// 本地工具调用参数
name := "leon"
payload, err := json.Marshal(map[string]string{"name": name})
if err != nil {
log.Fatalf("marshal input payload error: %v", err)
}
// Agent 是否调用工具,取决于 LLM 的决策。为了更确定让它使用工具,把问题换成明确的指令
// OneShotAgent 的输出解析器期望 LLM 的最终回答包含特定标记(通常为“Final Answer:”)
prompt := fmt.Sprintf("必须调用工具 greet,并传入参数 %s。完成后以 Final Answer: 为前缀原样返回工具输出,不要添加任何其他内容。", string(payload))
result, err := chains.Run(
ctx,
executor,
prompt,
)
if err != nil {
log.Fatalf("Agent execution error: %v, prompt:%s", err, prompt)
}
log.Println("agent result:", result)
// 调用高德地图查询天气
city := "杭州"
weatherPrompt := fmt.Sprintf("明天%s的天气怎么样?", city)
result, err = chains.Run(
ctx,
executor,
weatherPrompt,
)
if err != nil {
log.Fatalf("Agent execution error: %v, prompt:%s", err, weatherPrompt)
}
log.Println("agent result2:", result)
}
// initLLM 初始化OpenAI LLM模型
func initLLM() (llms.Model, error) {
llm, err := openai.New(
openai.WithToken(os.Getenv("OPENAI_API_KEY")),
openai.WithBaseURL(os.Getenv("OPENAI_API_BASE_URL")),
openai.WithModel(os.Getenv("OPENAI_MODEL")),
)
if err != nil {
return nil, fmt.Errorf("initLLM error: %w", err)
}
return llm, nil
}
// 启动本地MCP服务器
func connectLocalMCPServer(ctx context.Context) (*mcp.ClientSession, error) {
serverDir := "../server"
executableName := getExecutableName("server")
serverBinary := filepath.Join(serverDir, executableName)
// Check if binary exists
if _, err := os.Stat(serverBinary); os.IsNotExist(err) {
fmt.Println("Building MCP server binary...")
cmd := exec.Command("go", "build", "-o", executableName, "main.go")
cmd.Dir = serverDir
if output, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("build server: %w\nOutput: %s", err, output)
}
fmt.Println("MCP server binary built successfully")
}
fmt.Printf("Starting MCP server from: %s\n", serverBinary)
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
// Connect to a server over stdin/stdout.
transport := &mcp.CommandTransport{Command: exec.Command(serverBinary)}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
return nil, fmt.Errorf("connect to mcp server: %w", err)
}
return session, nil
}
// getExecutableName returns the executable name with proper extension for the OS
func getExecutableName(base string) string {
if runtime.GOOS == "windows" {
return base + ".exe"
}
return base
}
// 连接高德地图MCP服务器
func connectAmapMCPServer(ctx context.Context) (*mcp.ClientSession, error) {
amapApiKey := os.Getenv("AMAP_API_KEY")
ampURL := fmt.Sprintf("https://mcp.amap.com/mcp?key=%s", amapApiKey)
//log.Println("ampURL:", ampURL)
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
transport := &mcp.StreamableClientTransport{Endpoint: ampURL}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
return nil, fmt.Errorf("connect to amap mcp server: %w", err)
}
return session, nil
}
执行结果:
leon@leondeMacBook-Pro agent % go run main.go
Building MCP server binary...
MCP server binary built successfully
Starting MCP server from: ../server/server
agent result: {"greeting":"[MCP] Hello leon"}
agent result2: 明天杭州的天气预计为白天小雨,夜间多云,气温在15°C到19°C之间,风向为北风,风力1-3级。建议出门携带雨具,注意保暖。
环境准备与运行步骤
- 安装依赖与设置 Go 版本
go version # 建议 Go 1.24+ go mod tidy - 配置环境变量(OpenAI 示例)
export OPENAI_API_KEY=your_key export OPENAI_API_BASE_URL=https://api.openai.com/v1 export OPENAI_MODEL=gpt-4o-mini - 运行 Agent 示例(会自动构建并启动示例服务端)
cd example/agent && go run ./main.go - 单独运行 MCP 服务端
go run ./example/server
常见问题与排查
- 工具调用报错
invalid JSON input- 确保向
Tool.Call()传入的参数是合法 JSON 字符串,例如:{"name":"leon"}。
- 确保向
- 返回结果为空或
nil result- 检查 MCP 服务端的工具实现是否返回了
TextContent;适配器会把文本内容拼接为字符串返回。
- 检查 MCP 服务端的工具实现是否返回了
- 长时间无响应
- 使用
WithToolTimeout设置合理超时,或检查网络与服务端日志。
- 使用
- Agent 无法“决定”调用工具
- 检查 LLM 的提示词、Agent 类型与工具描述;
Description()中包含InputSchema能提升工具被选择的概率。
- 检查 LLM 的提示词、Agent 类型与工具描述;
- 远端 HTTP 流式连接失败
- 确认 Endpoint 可用、鉴权参数正确、代理/网络设置无误。
总结
通过一个轻量的适配器,我们把 MCP 的工具生态无缝接入到了 LangChainGo。由此你可以更快构建具备工具使用能力的智能体:标准化发现工具、统一接口调用、可控超时与清晰的输入模式,让工程实践更可靠。
如果你在使用场景中需要把内部服务封装成 MCP 工具,再用 LangChainGo 统一编排调用,上文的设计与代码可以直接作为参考模板。
项目源码
GitHub 仓库: