首页 › tutorials › Go构建MCP服务器:stdio到Streamable HTTP

Go 构建 MCP 服务器:从 stdio 本地工具到 Streamable HTTP 远程部署

概述

本教程面向已熟悉一种后端语言、想让 Claude Code 调用自建工具链的开发者。你将从零搭建一个用 Go 编写的 MCP(Model Context Protocol)服务器,先以 stdio 传输作为本地子进程接入 Claude Code,再迁移到 2025-03-26 规范引入的 Streamable HTTP 传输,把它部署成可被团队、云端、跨机器共享的远程服务。教程同时对比官方 modelcontextprotocol/go-sdk(已发布 v1.0.0)与社区 mark3labs/mcp-go 两套 SDK 的取舍,并给出生产加固(CORS、OAuth 受保护资源元数据、限流中间件、会话清理)的落地代码。

前置条件

一、为什么用 Go 写 MCP 服务器

绝大多数入门教程用 Python(FastMCP)或 TypeScript(@modelcontextprotocol/sdk)起步。它们上手快,但在"把 MCP 当成长期服务运行"时,Go 有三个结构性优势:

  1. 冷启动与延迟:Go 编译成单个原生二进制,没有解释器预热,也没有 Python 在每次 tools/call 时重复加载依赖的开销。对 stdio 模式尤其关键——Claude Code 会在需要时反复 spawn 你的进程,启动越快,首次调用越顺滑。
  2. 真并发:Python 受 GIL 约束,asyncio 在阻塞型 IO(如读大文件、查数据库)面前需要格外小心。Go 的 goroutine 天生适合"一个请求一个协程",配合 context.Context 做超时与取消非常自然。
  3. 分发即部署:go build 产出一个无依赖的二进制,扔进 Docker scratch 镜像只有十几兆,团队共享和 CI 流水线集成成本极低。

代价是:Go 的 MCP 生态比 Python/TS 年轻,配套的"脚手架生成器"较少。但官方 Go SDK 已在 2025 年 9 月中旬推出并达到 v1.0.0,能力已足够覆盖生产场景。

二、两套 SDK:官方 go-sdk vs mcp-go

维度 modelcontextprotocol/go-sdk(官方) mark3labs/mcp-go(社区)
维护方 Anthropic 协作维护,规范跟随最快 社区主导,迭代激进
工具定义 泛型 AddTool[In, Out],struct tag 自动生成 JSON Schema mcp.NewTool + 链式选项 WithString/WithNumber
传输 stdio / Streamable HTTP / SSE / Command stdio / Streamable HTTP / SSE
生产特性 较精简,留给你自己组装 内置中间件、CORS、OAuth 元数据、采样、会话清理
心智负担 低,类型安全 中,API 表面积大但开箱即用

选型建议:学习与新项目首选官方 SDK,代码干净、泛型让 schema 与 handler 强绑定,重构时编译器替你兜底;当你需要"装上即用"的中间件链、RFC 9728 OAuth 受保护资源元数据、跨域、采样等成熟能力时,再评估 mcp-go。本教程主线用官方 SDK 走完全流程,第七节用 mcp-go 演示生产加固。

三、环境准备

mkdir logseek && cd logseek
go mod init github.com/yourname/logseek
go get github.com/modelcontextprotocol/go-sdk

创建一个示例日志目录,方便后面测试:

mkdir -p logs
echo "2026-06-16T08:12:01 INFO  用户 alice 登录" > logs/2026-06-16.log
echo "2026-06-16T08:13:44 ERROR 数据库连接超时 timeout=5s" >> logs/2026-06-16.log
echo "2026-06-16T08:14:02 WARN  重试第 2 次" >> logs/2026-06-16.log

四、Step 1:用官方 go-sdk 写第一个工具(stdio)

新建 main.go,实现一个"按正则检索某天日志"的工具,并用 stdio 传输跑起来。

package main

import (
    "bufio"
    "context"
    "flag"
    "fmt"
    "log"
    "net/http"
    "os"
    "path/filepath"
    "regexp"
    "strconv"
    "strings"
    "time"

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

// --- 工具入参:struct tag 自动生成 JSON Schema 供客户端(LLM)理解 ---
type SearchParams struct {
    Pattern string `json:"pattern" jsonschema:"正则或关键字"`
    Date    string `json:"date"    jsonschema:"日期 YYYY-MM-DD,留空表示今天"`
    Limit   int    `json:"limit"   jsonschema:"返回行数上限,默认 50"`
}

type CountParams struct {
    Pattern string `json:"pattern"`
    Date    string `json:"date"`
}

func logDir() string {
    if d := os.Getenv("LOGSEEK_DIR"); d != "" {
        return d
    }
    return "logs"
}

func today() string { return time.Now().Format("2006-01-02") }

// --- handler 签名固定:泛型版 ToolHandlerFor[In] ---
func searchLogs(ctx context.Context, _ *mcp.ServerSession,
    p *mcp.CallToolParamsFor[SearchParams]) (*mcp.CallToolResultFor[any], error) {

    date := p.Arguments.Date
    if date == "" {
        date = today()
    }
    path := filepath.Join(logDir(), date+".log")

    f, err := os.Open(path)
    if err != nil {
        // 返回 error 会被 SDK 转成 tool 级错误(IsError=true),不会让进程崩溃
        return nil, fmt.Errorf("打开日志失败 %s: %w", path, err)
    }
    defer f.Close()

    re, err := regexp.Compile(p.Arguments.Pattern)
    if err != nil {
        return nil, fmt.Errorf("非法正则: %w", err)
    }
    limit := p.Arguments.Limit
    if limit <= 0 {
        limit = 50
    }

    var hits []string
    sc := bufio.NewScanner(f)
    sc.Buffer(make([]byte, 1<<20), 1<<20) // 单行最大 1MB,避免超长行截断
    for sc.Scan() {
        if ctx.Err() != nil { // 响应客户端取消
            return nil, ctx.Err()
        }
        line := sc.Text()
        if re.MatchString(line) {
            hits = append(hits, line)
            if len(hits) >= limit {
                break
            }
        }
    }

    text := fmt.Sprintf("命中 %d 行(上限 %d):\n%s",
        len(hits), limit, strings.Join(hits, "\n"))
    return &mcp.CallToolResultFor[any]{
        Content: []mcp.Content{&mcp.TextContent{Text: text}},
    }, nil
}

func countLogs(ctx context.Context, _ *mcp.ServerSession,
    p *mcp.CallToolParamsFor[CountParams]) (*mcp.CallToolResultFor[any], error) {

    date := p.Arguments.Date
    if date == "" {
        date = today()
    }
    data, err := os.ReadFile(filepath.Join(logDir(), date+".log"))
    if err != nil {
        return nil, err
    }
    re, err := regexp.Compile(p.Arguments.Pattern)
    if err != nil {
        return nil, err
    }
    n := len(re.FindAll(data, -1))
    return &mcp.CallToolResultFor[any]{
        Content: []mcp.Content{
            &mcp.TextContent{Text: strconv.Itoa(n)},
        },
    }, nil
}

func newServer() *mcp.Server {
    s := mcp.NewServer(&mcp.Implementation{Name: "logseek", Version: "1.0.0"}, nil)
    mcp.AddTool(s,
        &mcp.Tool{Name: "search_logs", Description: "按正则搜索某天日志的匹配行"},
        searchLogs)
    mcp.AddTool(s,
        &mcp.Tool{Name: "count_logs", Description: "统计某天日志中某模式的出现次数"},
        countLogs)
    return s
}

func main() {
    mode := flag.String("mode", "stdio", "stdio | http")
    flag.Parse()

    s := newServer()

    switch *mode {
    case "http":
        runHTTP(s)
    default:
        // 阻塞直到客户端断开或 context 取消
        if err := s.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
            log.Fatal(err)
        }
    }
}

func runHTTP(s *mcp.Server) {
    handler := mcp.NewStreamableHTTPHandler(
        func(*http.Request) *mcp.Server { return s },
        &mcp.StreamableHTTPOptions{},
    )
    http.HandleFunc("/mcp", handler.ServeHTTP)
    log.Println("logseek HTTP 监听 :8080/mcp")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

几个关键点:

接入 Claude Code(stdio 模式)

先编译出二进制并放到 PATH,或直接用绝对路径:

go build -o logseek .

在项目根目录添加服务器,使用 stdio 传输:

claude mcp add --transport stdio logseek -- /home/yourname/logseek/logseek

-- 之后是真正启动进程的命令,Claude Code 不会把它当自己的参数解析。配置默认写入 ~/.claude.json 的 local scope(仅当前项目、私有)。验证:

claude mcp list
claude mcp get logseek

进入交互会话用 /mcp 查看连接状态,应看到 logseek 后面带工具数 2。然后直接提问:

帮我看看 2026-06-16 的日志里有没有 ERROR,统计一下出现了几次。

Claude 会自行决定调用 search_logs(pattern=ERROR)和 count_logs,你无需手动指定工具。

常见问题

五、Step 2:迁移到 Streamable HTTP

stdio 适合"个人本地工具",但它把进程绑定在你本机,无法跨机器、无法被团队共享、无法做集中式鉴权。把同一个服务器切换到 Streamable HTTP 传输,它就变成了一个普通的远程 web 服务。

关键背景:MCP 规范 2025-03-26 版本用 Streamable HTTP 取代了旧的 HTTP+SSE 传输。旧方案要两个端点(/sse 接收、/sse/messages 发送),像用两部手机对话;新方案只用单个端点(通常是 /mcp),既能像普通 HTTP 请求/响应那样处理快速调用,又能在长任务时动态升级成 SSE 流,还能让服务器反向通过同一连接发通知(采样、进度)。旧的 SSE 传输已标记废弃。

好消息是:上文的 runHTTP 已经是 Streamable HTTP 实现,-mode http 即可:

./logseek -mode http
# logseek HTTP 监听 :8080/mcp

用 Claude Code 接入远程服务器:

claude mcp add --transport http logseek http://localhost:8080/mcp

等价的项目级配置(写入项目根的 .mcp.json,可提交版本控制与团队共享):

{
  "mcpServers": {
    "logseek": {
      "type": "http",
      "url": "http://localhost:8080/mcp"
    }
  }
}

type 字段同时接受 http 和 streamable-http,后者与 MCP 规范用词一致,所以从服务器文档里复制过来的配置无需改动。带鉴权头时:

claude mcp add --transport http logseek https://mcp.yourcorp.com/mcp \
  --header "Authorization: Bearer ${LOGSEEK_TOKEN}"

.mcp.json 里支持环境变量展开 "url": "${LOGSEEK_BASE:-https://mcp.yourcorp.com}/mcp" 与 "headers": {"Authorization": "Bearer ${LOGSEEK_TOKEN}"},敏感值不落库。

需要 OAuth 时,无需手动配置流程:Claude Code 在服务器返回 401/403 时自动把它标记为"需认证",在 /mcp 面板里走浏览器登录;服务器若按 RFC 9728 暴露 /.well-known/oauth-protected-resource,会被自动发现。

六、Streamable HTTP 的关键机制

理解下面四点,调试和设计远程 MCP 时就不会抓瞎。

1. 单端点 + 会话 ID。 客户端 POST /mcp 发 initialize,服务器可选地回一个 Mcp-Session-Id 响应头(如 abc-123)。此后每次请求,客户端都要在 Mcp-Session-Id 请求头里带上它,服务器据此把同一会话的工具列表、订阅、日志级别等状态归并起来。

2. 动态降级/升级。 简单调用就是一次普通 POST,返回 application/json;如果调用过程中服务器要推进度通知,同一个 POST 响应会被升级成 text/event-stream(SSE);客户端想持续监听服务器主动推送(如采样请求),可发 GET /mcp 建立长连接。DELETE /mcp 结束会话。这正是"Streamable"的含义——按需流式,不必为短查询维持长连接。

3. 有状态 vs 无状态。 默认有状态:会话内可累积上下文。横向扩展(多副本、负载均衡)时,要么开启粘性路由(同一 session 落到同一实例),要么走无状态模式——每个请求自包含所有信息,状态外置到 Redis/数据库。无状态模式放弃会话亲和,换来任意副本可处理的水平扩展能力。

4. 会话清理。 客户端断开却没发 DELETE,会话状态会泄漏内存。生产环境务必设置空闲 TTL,定期清扫长时间无请求的会话并使其 ID 失效。

七、生产加固(用 mcp-go 演示成熟能力)

官方 SDK 刻意精简,把组装权留给你。若你想"装上即用",mark3labs/mcp-go 提供了一整套生产选项。下面代码切换到 mcp-go(go get github.com/mark3labs/mcp-go),同样的服务器,加固四件套:

package main

import (
    "context"
    "log"
    "net/http"
    "time"

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

func main() {
    s := server.NewMCPServer("logseek", "1.0.0",
        server.WithToolCapabilities(true),
        server.WithResourceCapabilities(true, true),
        server.WithLogging(),
    )

    s.AddTool(mcp.NewTool("search_logs",
        mcp.WithDescription("按正则搜索某天日志"),
        mcp.WithString("pattern", mcp.Required()),
        mcp.WithString("date"),
        mcp.WithNumber("limit", mcp.DefaultNumber(50), mcp.Max(500)),
    ), handleSearch)

    // ① 工具中间件链:鉴权 + 限流 + 缓存
    s.AddToolMiddleware(func(next server.ToolHandler) server.ToolHandler {
        return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            token := req.Header.Get("Authorization") // 传输层自动透传 HTTP 头
            if token == "" {
                return mcp.NewToolResultError("authentication required"), nil
            }
            return next(ctx, req)
        }
    })

    // ② Streamable HTTP 服务器,含端点、会话清理、CORS、OAuth 元数据
    httpServer := server.NewStreamableHTTPServer(s,
        server.WithEndpointPath("/mcp"),
        server.WithStateLess(false),
        server.WithSessionIdleTTL(10*time.Minute), // 清扫空闲 10 分钟的会话
        server.WithStreamableHTTPCORS(
            server.WithCORSAllowedOrigins("https://app.yourcorp.com"),
            server.WithCORSAllowCredentials(),
            server.WithCORSMaxAge(300),
        ),
        server.WithProtectedResourceMetadata(server.ProtectedResourceMetadataConfig{
            Resource:            "https://mcp.yourcorp.com/mcp",
            AuthorizationServers: []string{"https://auth.yourcorp.com"},
            ScopesSupported:     []string{"logs:read"},
            BearerMethodsSupported: []string{"header"},
        }),
    )
    log.Println("生产 logseek 监听 :8080/mcp")
    log.Fatal(httpServer.Start(":8080"))
}

func handleSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // req.GetString("pattern", ""), req.GetInt("limit", 50) …
    return mcp.NewToolResultText("..."), nil
}

四个加固点的意义:

采样(sampling)——服务器反向请求客户端的 LLM 能力——也已被 Streamable HTTP 支持,且规范强制要求"人在回路"审批。生产中若开启 mcpServer.EnableSampling(),务必实现一个会向用户展示并允许拒绝的采样 handler,否则服务器可能在你不知情时以你的名义产生 LLM 调用与费用。

八、完整实战:日志检索服务 logseek 的端到端流程

把上面拼起来,一个可交付的最小服务长这样:本地用 stdio 开发调试 → 编译成单二进制 → 容器化以 HTTP 部署 → 团队经 Claude Code 接入。

Dockerfile(scratch 镜像,约 15MB):

FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/logseek .

FROM gcr.io/distroless/static
COPY --from=build /out/logseek /logseek
ENV LOGSEEK_DIR=/data
EXPOSE 8080
ENTRYPOINT ["/logseek", "-mode", "http"]
docker build -t logseek .
docker run -p 8080:8080 -v $(pwd)/logs:/data logseek
claude mcp add --transport http logseek http://localhost:8080/mcp

架构与交互图

flowchart LR
  subgraph 本地开发[本地开发 stdio]
    CC1[Claude Code] -->|spawn + stdin/stdout| LS1[logseek 二进制]
    LS1 --> F1[(本地 logs/)]
  end
  subgraph 远程部署[团队/云端 Streamable HTTP]
    CC2[Claude Code] -->|POST /mcp\nMcp-Session-Id| GW[网关: CORS+鉴权+OAuth元数据]
    GW --> LS2[logseek HTTP :8080]
    LS2 --> F2[(共享日志卷)]
  end
sequenceDiagram
    participant CC as Claude Code
    participant S as logseek /mcp
    CC->>S: POST /mcp  initialize
    S-->>CC: 200  Mcp-Session-Id: abc-123
    CC->>S: POST /mcp  tools/list  (Header: Mcp-Session-Id)
    S-->>CC: search_logs, count_logs
    CC->>S: POST /mcp  tools/call search_logs {pattern:"ERROR"}
    Note over S: 命中行多 → 响应升级为 text/event-stream
    S-->>CC: 200  命中结果(流式分块)
    CC->>S: DELETE /mcp  结束会话
    S-->>CC: 200

Claude Code 侧的运维要点

九、常见问题汇总

延伸阅读

训练营适用性评估

参考来源

  1. StreamableHTTP Transport – MCP-Go 官方文档 — mcp-go 的 StreamableHTTP 实现细节、端点、会话管理、CORS、OAuth 元数据、采样与 fasthttp 适配,代码示例来源。
  2. Why MCP Deprecated SSE and Went with Streamable HTTP – fka.dev — SSE 双端点模型的局限、Streamable HTTP 单端点设计、规范版本 2025-03-26 与 TypeScript SDK 1.10.0 时间线。
  3. Getting Started – modelcontextprotocol/go-sdk DeepWiki — 官方 Go SDK 的 NewServer/AddTool/Run、StdioTransport、泛型 ToolHandlerFor 与 struct tag 自动生成 schema 的权威说明。
  4. How to Build an MCP Server in Go: A Complete Guide – ContexaAI — 官方 go-sdk 的泛型 handler 签名、NewStreamableHTTPHandler 用法、Go 相对 Python 的性能优势。
  5. Connect Claude Code to tools via MCP – Claude Code Docs — claude mcp add --transport http、.mcp.json 配置、scope 层级、OAuth 自动发现、工具搜索、大输出与 anthropic/maxResultSizeChars 等客户端侧行为。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入