使用 LangChainGo 调用 MCP 工具

146 阅读8分钟

用 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,并实现 NameDescriptionCall
  • 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,传入 Agent
  • chains.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级。建议出门携带雨具,注意保暖。

环境准备与运行步骤

  1. 安装依赖与设置 Go 版本
    go version           # 建议 Go 1.24+
    go mod tidy
    
  2. 配置环境变量(OpenAI 示例)
    export OPENAI_API_KEY=your_key
    export OPENAI_API_BASE_URL=https://api.openai.com/v1
    export OPENAI_MODEL=gpt-4o-mini
    
  3. 运行 Agent 示例(会自动构建并启动示例服务端)
    cd example/agent && go run ./main.go
    
  4. 单独运行 MCP 服务端
    go run ./example/server
    

常见问题与排查

  • 工具调用报错 invalid JSON input
    • 确保向 Tool.Call() 传入的参数是合法 JSON 字符串,例如:{"name":"leon"}
  • 返回结果为空或 nil result
    • 检查 MCP 服务端的工具实现是否返回了 TextContent;适配器会把文本内容拼接为字符串返回。
  • 长时间无响应
    • 使用 WithToolTimeout 设置合理超时,或检查网络与服务端日志。
  • Agent 无法“决定”调用工具
    • 检查 LLM 的提示词、Agent 类型与工具描述;Description() 中包含 InputSchema 能提升工具被选择的概率。
  • 远端 HTTP 流式连接失败
    • 确认 Endpoint 可用、鉴权参数正确、代理/网络设置无误。

总结

通过一个轻量的适配器,我们把 MCP 的工具生态无缝接入到了 LangChainGo。由此你可以更快构建具备工具使用能力的智能体:标准化发现工具、统一接口调用、可控超时与清晰的输入模式,让工程实践更可靠。

如果你在使用场景中需要把内部服务封装成 MCP 工具,再用 LangChainGo 统一编排调用,上文的设计与代码可以直接作为参考模板。


项目源码

GitHub 仓库: