前言:从理论迈向实操

之前写过《MCP 是什么?一次搞懂 AI 如何“标准化接入世界”》一文,从宏观层面剖析了 Model Context Protocol (MCP) 的核心目标——消除 AI 模型与外部数据之间的信息壁垒。

然而,“纸上得来终觉浅,绝知此事要躬行”

深入理解协议的最佳途径莫过于亲手实现一个版本。尽管官方和社区已经提供了多种现成服务端(如对接 Google Drive、GitHub 的工具),但对于开发者而言,掌握从零构建符合 MCP 规范的服务能力,才能真正把握"标准化接入"的主动权。

Go 语言凭借其极其出色的并发模型、简洁的二进制分发能力以及较低的内存占用,天然适合编写轻量级、高性能的 MCP Server。今天我就来一场实战:如何用 Go 语言手动构建一个 SQLite MCP Server

这个目标很简单:通过这个 Server,让 AI(如 Claude Desktop)能够直接读取、查询并分析你本地的 SQLite 数据库文件。

一、工欲善其事,必先利其器

在动手写逻辑之前,需要解决两个核心的“技术选型”问题。

1. SDK 选型:官方标准还是社区封装?

目前在 Go 生态中,实现 MCP 主要有两个主流选择:

  • 官方版本:github.com/modelcontextprotocol/go-sdk
    • 优势:权威性最高,协议定义最严谨。如果 MCP 协议层有任何更新,官方 SDK 理论上会第一时间同步。
    • 劣势:目前仍处于早期阶段,API 设计相对偏向底层,对开发者不够“友好”,需要手动处理较多的底层对象转换。
  • 社区版本:github.com/mark3labs/mcp-go
    • 优势:这是目前社区中最好用的封装。它在官方 SDK 的基础上进行了高度抽象,提供了大量“糖衣语法”(Syntactic Sugar)。注册一个 Tool 就像定义一个普通函数一样简单,极大地降低了开发者的心智负担。
    • 劣势:作为第三方库,其更新节奏依赖于维护者的活跃度(不过目前该库非常活跃)。

💡 笔者的建议:
如果你是构建企业级的核心基础设施,且对协议细节有极端定制需求,选官方版;如果你是为了快速实现业务逻辑、构建实用的 AI 工具,我强烈建议使用 mark3labs/mcp-go

本篇教程将基于 mark3labs/mcp-go 进行实战。

2. 传输模式:Stdio 还是 HTTP?

在 MCP 的架构中,Server 与 Client 之间的通信主要有两条路径:

  • 路径 A:Stdio(标准输入输出)

    if err := server.ServeStdio(s); err != nil {
        log.Fatalf("Server Error: %v", err)
    }
    

    这是目前本地 AI 应用(如 Claude Desktop)最常用的方式。Client 启动 Server 作为一个子进程,通过标准 IO 流进行 JSON-RPC 通信。它不需要占用网络端口,配置简单,安全性高(仅本地访问)。

  • 路径 B:HTTP (SSE 模式)
    这是基于 HTTP 的 Server-Sent Events 实现。它允许 Server 以独立进程运行在本地或云端,Client 通过网络请求进行连接。这适合分布式或远程连接场景。

无论是 Stdio 还是 HTTP,MCP Server 与 Client 之间沟通的本质都是 JSON-RPC 2.0

  • 什么是 JSON-RPC? 它是一种简单的远程过程调用协议。结构非常固定,通常包含 jsonrpc 版本号、method(调用的方法名)、params(参数)和 id(请求标识)。
  • Stdio 里的数据长什么样? 在 Stdio 模式下,Claude 启动你的 Go 程序后,会通过 stdin 给你的程序发送一行 JSON 文本,你的程序处理完后,必须通过 stdout 回传一行 JSON 响应。
    • Client 发送: {"jsonrpc":"2.0","method":"tools/call","params":{"name":"sqlite_query","arguments":{"query":"SELECT..."}},"id":1}
    • Server 响应: {"jsonrpc":"2.0","result":{"content":[{"type":"text","text":"..."}]},"id":1}
  • 致命的 Println: 看到这里你就明白了,如果你在代码里不小心写了一句 fmt.Println("数据库连接成功"),这行字符串会直接混入 stdout 的 JSON 流中。Claude 的解析器会因为“发现非法的 JSON 字符”而直接报错断开连接。

二、核心实现 —— 编写 Go 版 SQLite MCP Server

有了 mark3labs/mcp-go SDK,我们可以像写普通 Web 接口一样,快速定义 AI 调用的“工具”。在实现过程中,有几个关键的技术点需要注意:

2.1 为什么选择纯 Go 驱动?

在代码中,我们引入了 _ "modernc.org/sqlite"

  • 痛点:传统的 github.com/mattn/go-sqlite3 依赖 CGO,在跨平台编译或在没有 gcc 环境的容器中运行非常麻烦。
  • 优势modernc.org/sqlite 是纯 Go 实现,无须 CGO,让我们的 MCP Server 真正做到“一次编译,到处运行”。

2.2 定义工具(Tools)

MCP 的核心是让 AI 明白它能做什么。在代码中,我们定义了两个工具:

  • sqlite_list_tables:让 AI 获取数据库结构,避免它胡乱猜测表名。
  • sqlite_query:这是 AI 的“手”,允许它执行真实的 SELECT 语句。

代码片段:

listTool := mcp.Tool{
    Name: "sqlite_list_tables",
    Description: `列出本地 SQLite 数据库中所有的表。AI 在询问表名时必须使用此工具。`,
    InputSchema: mcp.ToolInputSchema{
        Type: "object",
        Properties: map[string]interface{}{}, // 简单起见,可以无入参
    },
}
s.AddTool(listTool, listTablesHandler)

2.3 处理 Stdio 通信与日志陷阱

这是新手最容易踩坑的地方。因为 MCP Server 通过 stdin/stdout 与 Claude 通信,你绝对不能在代码里直接使用 fmt.Printlnlog.Print 打印调试信息到控制台,否则会破坏 JSON-RPC 协议格式,导致连接中断。

解决方案: 像代码里展示的那样,将日志重定向到本地文件:

logFile, _ := os.OpenFile("/tmp/mcp-sqlite-debug.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
log.SetOutput(logFile) // 所有调试日志都去这里看,不要干扰 stdout

2.4 代码 demo

package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	_ "modernc.org/sqlite" // <--- 关键修改:使用纯 Go 驱动
)

// 请把这个路径改成你本机真实的 test.db 路径!
const DB_PATH = "/Users/src/go/sqlite_mcp_server/test.db"
const LOG_PATH = "/tmp/mcp-sqlite-debug.log"

func main() {
	// 1. 日志设置 (防止 stdout 污染)
	logFile, err := os.OpenFile(LOG_PATH, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err == nil {
		log.SetOutput(logFile)
	}
	log.Println("=== Server Started (Pure Go Mode) ===")

	// 2. 初始化数据库
	initDB()

	// 3. 创建 Server
	s := server.NewMCPServer("Go-SQLite-Explorer", "1.0.0")

	// --- 关键修改:使用结构体直接定义工具,避免 helper 函数报错 ---

	// 工具 1: 列出表
	listTool := mcp.Tool{
		Name: "sqlite_list_tables",
		Description: `
List all tables from the user's LOCAL SQLite database.
This tool reads REAL database metadata at runtime.
The assistant MUST use this tool when asked about existing tables.
Do NOT guess or fabricate table names.
`,
		InputSchema: mcp.ToolInputSchema{
			Type: "object",
			Properties: map[string]interface{}{
				// 加一个可选的虚假参数,有助于 Claude 正确渲染授权框
				"dummy": map[string]interface{}{
					"type":        "string",
					"description": "optional placeholder",
				},
			},
		},
	}
	s.AddTool(listTool, listTablesHandler)

	// 工具 2: 执行查询
	queryTool := mcp.Tool{
		Name: "sqlite_query",
		Description: `
Execute a READ-ONLY SELECT query against the user's LOCAL SQLite database.
This is the ONLY way to retrieve REAL data.
The assistant MUST use this tool to answer any question involving database contents.
Do NOT guess, simulate, or fabricate query results.
Only SELECT statements are allowed.
`,
		InputSchema: mcp.ToolInputSchema{
			Type: "object",
			Properties: map[string]interface{}{
				"query": map[string]interface{}{
					"type":        "string",
					"description": "A READ-ONLY SELECT SQL query to execute against the local SQLite database",
				},
			},
			Required: []string{"query"},
		},
	}
	s.AddTool(queryTool, queryHandler)

	// 4. 启动服务
	if err := server.ServeStdio(s); err != nil {
		log.Printf("Server Critical Error: %v", err)
		os.Exit(1)
	}
}

func initDB() {

	db, err := sql.Open("sqlite", DB_PATH)
	if err != nil {
		log.Printf("Init Open Error: %v", err)
		return
	}
	defer db.Close()

	// 简单的建表确保库不是空的
	sqlStmt := `
	CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, role TEXT, salary REAL);
	INSERT OR IGNORE INTO users (id, name, role, salary) VALUES (1, 'Alice', 'Dev', 120000);
	`
	_, err = db.Exec(sqlStmt)
	if err != nil {
		log.Printf("Init Exec Error: %v", err)
	}
}

func listTablesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	log.Println("Tool called: sqlite_list_tables")

	db, err := sql.Open("sqlite", DB_PATH)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("DB Open Error: %v", err)), nil
	}
	defer db.Close()

	rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table'")
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Query Error: %v", err)), nil
	}
	defer rows.Close()

	var tables []string
	for rows.Next() {
		var name string
		rows.Scan(&name)
		tables = append(tables, name)
	}

	// 返回结果
	return mcp.NewToolResultText(fmt.Sprintf("Tables: %v", tables)), nil
}

func queryHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	log.Println("Tool called: sqlite_query")

	// 参数解析 (防御性编程)
	args, ok := request.Params.Arguments.(map[string]interface{})
	if !ok {
		return mcp.NewToolResultError("Invalid arguments"), nil
	}
	queryVal, ok := args["query"]
	if !ok {
		return mcp.NewToolResultError("Missing query"), nil
	}
	query, ok := queryVal.(string)
	if !ok {
		return mcp.NewToolResultError("Query must be string"), nil
	}

	// 执行
	db, err := sql.Open("sqlite", DB_PATH)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("DB Error: %v", err)), nil
	}
	defer db.Close()

	rows, err := db.Query(query)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("SQL Error: %v", err)), nil
	}
	defer rows.Close()

	// 转换结果为 JSON 字符串
	cols, _ := rows.Columns()
	var resultData []map[string]interface{}

	for rows.Next() {
		columns := make([]interface{}, len(cols))
		columnPointers := make([]interface{}, len(cols))
		for i := range columns {
			columnPointers[i] = &columns[i]
		}

		if err := rows.Scan(columnPointers...); err != nil {
			return mcp.NewToolResultError("Scan error"), nil
		}

		m := make(map[string]interface{})
		for i, colName := range cols {
			val := columnPointers[i].(*interface{})
			m[colName] = *val
		}
		resultData = append(resultData, m)
	}

	jsonBytes, _ := json.MarshalIndent(resultData, "", "  ")
	return mcp.NewToolResultText(string(jsonBytes)), nil
}

三、接入 Claude —— 赋予 AI “读库”能力

编写完代码并执行 go build -o mcp-server 编译出二进制文件后,剩下的就是将其接入 AI 客户端(如 Claude Desktop)。

3.1 配置 Claude Desktop

我们需要修改 Claude 的全局配置文件。

  • macOS 路径~/Library/Application\ Support/Claude/claude_desktop_config.json
  • Windows 路径%APPDATA%\Claude\claude_desktop_config.json

在配置文件中,我们添加刚才编译好的 Go 程序路径:

{
    "mcpServers": {
        "mysql": {
            "type": "stdio",
            "command": "node",
            "args": [
                "/opt/homebrew/lib/node_modules/@benborla29/mcp-server-mysql/dist/index.js"
            ],
            "cwd": "/opt/homebrew/lib/node_modules/@benborla29/mcp-server-mysql",
            "env": {
                "MYSQL_HOST": "127.0.0.1",
                "MYSQL_PORT": "3306",
                "MYSQL_USER": "root",
                "MYSQL_PASS": "******",
                "MYSQL_DB": "demo"
            }
        },
        "my-go-sqlite": {
            "command": "/Users/src/go/sqlite_mcp_server/mcp-server",
            "args": [

            ]
        }
    }
}

3.2 启动与验证

重启 Claude Desktop。如果配置正确,点击对话框右下角的 Connectors(插件图标),你应该能看到 my-go-sqlite 已经亮起了小绿灯(如你所提供的截图所示)。

在这里插入图片描述

3.3 实战对话

现在,你可以尝试向 Claude 提问:

  • “我这个 sqlite 数据库里有哪些表?” (Claude 会自动调用 sqlite_list_tables)
  • “帮我查一下 users 表有哪些内容?” (Claude 会根据表结构自动生成 SQL,调用 sqlite_query 并给出结果)

在这里插入图片描述

四、进阶实战 —— 将 MCP Server 升级为 HTTP (SSE) 模式

在前面的章节中,我们实现了 Stdio 模式。虽然它简单,但在调试时如同“盲盒”:你无法直接看控制台输出,必须时刻盯着日志文件。

一旦我们将 Server 改为 HTTP 模式(基于 Server-Sent Events, SSE),开发体验将发生质变:你可以像写普通 Web 接口一样写 MCP,所有的调试信息都能直接打印在终端窗口里。

4.1 Go 代码实现:从子进程到 Web 服务

得益于 mark3labs/mcp-go 优秀的抽象,我们只需要修改 main 函数的启动逻辑,而业务逻辑(Handler)一行都不用改。

注意看最新的 Functional Options 写法:

package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "fmt"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
    "log"
    _ "modernc.org/sqlite"
    "net/http" // <--- 新增
)

const DB_PATH = "/Users/src/go/sqlite_mcp_server/test.db"

func main() {
    // 1. 初始化数据库
    initDB()

    // 2. 创建 MCP Server 实例
    s := server.NewMCPServer("Go-SQLite-Explorer-HTTP", "1.0.0")

    // 3. 注册工具 (这部分逻辑和你之前的一模一样)
    registerTools(s)

    // 4. --- 关键修改:改为 HTTP SSE 启动 ---

    // 创建 SSE 服务实例。第二个参数是基础 URL,Claude 会用它来拼接消息回调地址
    sse := server.NewSSEServer(s, server.WithBaseURL("http://localhost:8080"))

    // 设置 MCP 协议要求的两个核心路由
    http.Handle("/sse", sse.SSEHandler())
    http.Handle("/message", sse.MessageHandler())

    fmt.Println("MCP HTTP Server 正在启动...")
    fmt.Println("监听端口: :8080")
    fmt.Println("SSE 端点: http://localhost:8080/sse")

    // 启动 HTTP 服务
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalf("Server Error: %v", err)
    }
}

// 为了代码整洁,把工具注册抽离出来,内容和你之前的代码完全一致
func registerTools(s *server.MCPServer) {
    listTool := mcp.Tool{
        Name:        "sqlite_list_tables",
        Description: `List all tables from the user's LOCAL SQLite database.`,
        InputSchema: mcp.ToolInputSchema{
            Type: "object",
            Properties: map[string]interface{}{
                "dummy": map[string]interface{}{"type": "string"},
            },
        },
    }
    s.AddTool(listTool, listTablesHandler)

    queryTool := mcp.Tool{
        Name:        "sqlite_query",
        Description: `Execute a READ-ONLY SELECT query.`,
        InputSchema: mcp.ToolInputSchema{
            Type: "object",
            Properties: map[string]interface{}{
                "query": map[string]interface{}{"type": "string"},
            },
            Required: []string{"query"},
        },
    }
    s.AddTool(queryTool, queryHandler)
}

func initDB() {
    // 注意:这里驱动名变成了 "sqlite",不是 "sqlite3"
    db, err := sql.Open("sqlite", DB_PATH)
    if err != nil {
        log.Printf("Init Open Error: %v", err)
        return
    }
    defer db.Close()

    // 简单的建表确保库不是空的
    sqlStmt := `
    CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, role TEXT, salary REAL);
    INSERT OR IGNORE INTO users (id, name, role, salary) VALUES (1, 'Alice', 'Dev', 120000);
    `
    _, err = db.Exec(sqlStmt)
    if err != nil {
        log.Printf("Init Exec Error: %v", err)
    }
}

func listTablesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    log.Println("Tool called: sqlite_list_tables")

    // 注意:驱动名必须是 "sqlite"
    db, err := sql.Open("sqlite", DB_PATH)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("DB Open Error: %v", err)), nil
    }
    defer db.Close()

    rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table'")
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("Query Error: %v", err)), nil
    }
    defer rows.Close()

    var tables []string
    for rows.Next() {
        var name string
        rows.Scan(&name)
        tables = append(tables, name)
    }

    // 返回结果
    return mcp.NewToolResultText(fmt.Sprintf("Tables: %v", tables)), nil
}

func queryHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    log.Println("Tool called: sqlite_query")

    // 参数解析 (防御性编程)
    args, ok := request.Params.Arguments.(map[string]interface{})
    if !ok {
        return mcp.NewToolResultError("Invalid arguments"), nil
    }
    queryVal, ok := args["query"]
    if !ok {
        return mcp.NewToolResultError("Missing query"), nil
    }
    query, ok := queryVal.(string)
    if !ok {
        return mcp.NewToolResultError("Query must be string"), nil
    }

    // 执行
    db, err := sql.Open("sqlite", DB_PATH)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("DB Error: %v", err)), nil
    }
    defer db.Close()

    rows, err := db.Query(query)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("SQL Error: %v", err)), nil
    }
    defer rows.Close()

    // 转换结果为 JSON 字符串
    cols, _ := rows.Columns()
    var resultData []map[string]interface{}

    for rows.Next() {
        columns := make([]interface{}, len(cols))
        columnPointers := make([]interface{}, len(cols))
        for i := range columns {
            columnPointers[i] = &columns[i]
        }

        if err := rows.Scan(columnPointers...); err != nil {
            return mcp.NewToolResultError("Scan error"), nil
        }

        m := make(map[string]interface{})
        for i, colName := range cols {
            val := columnPointers[i].(*interface{})
            m[colName] = *val
        }
        resultData = append(resultData, m)
    }

    jsonBytes, _ := json.MarshalIndent(resultData, "", "  ")
    return mcp.NewToolResultText(string(jsonBytes)), nil
}

4.2 配置 Claude:巧妙使用 mcp-remote

在配置 Claude Desktop 时,虽然官方开始支持原生 SSE 配置,但目前最稳健、兼容性最好的方式是使用 mcp-remote 工具。

mcp-remote 就像一个“翻译官”,它对外表现为一个 Stdio 进程(符合 Claude 默认预期),对内则通过 HTTP 连接我们的 Go Server。

修改 claude_desktop_config.json

{
    "mcpServers": {
        "my-go-sqlite": {
            "command": "npx",
            "args": [   
                "-y",
                "mcp-remote",
                "http://localhost:8080/sse",
                "--allow-http"
            ]
        }
    }
}

💡 为什么这么配?

  • npx -y mcp-remote:无需预先安装,即用即走。
  • http://localhost:8080/sse:指向我们 Go 程序的 SSE 入口。
  • --allow-http:由于我们是在本地开发,没有配置 HTTPS,这个参数必不可少。

4.3 为什么强烈建议你尝试 HTTP 模式?

  1. 调试极其方便:你可以直接在 Go 代码里写 fmt.Println,所有请求解析、SQL 执行过程都会直接显示在你运行程序的终端里。不再需要去 tail -f 那个冷冰冰的日志文件。
  2. 服务热重启:在 Stdio 模式下,改一次代码就要重启一次 Claude(因为它要重新拉起子进程)。在 HTTP 模式下,你只需要重启 Go 程序,Claude 会自动尝试重连,开发效率翻倍。
  3. 多客户端共享:你的 SQLite 数据库现在不仅能给 Claude 用,由于它是一个网络服务,你甚至可以同时开启多个 AI 客户端(如 Cursor, Zed)连接到同一个 Server。

至此,咱们已经亲手构建了一个从 Stdio 到 HTTP 全面贯通的 Go 版 MCP Server。AI 不再只是一个聊天机器人,它现在拥有了阅读你本地数据库的能力!

五、工具治理 —— 如何实现 MCP 的“按需加载”?

当你像我一样,在 Claude 中同时挂载了 my-go-sqlitemysql 多个服务时,你会发现对话框右下角的“小插头”图标变得非常重要。

5.1 基础版:利用 Claude UI 进行手动开关(最直接的按需)

正如你在截图中所见,Claude Desktop 提供了原生的开关功能:

  • 操作:点击对话框右下角的 Connectors 图标。
  • 按需加载:你可以根据当前的对话任务,手动勾选或取消某个 Server。
  • 意义:这在物理层面减少了 AI 的上下文负担。如果你这节课只想分析本地 SQLite 表,那就把 MySQL 关掉。

5.2 进阶版:架构层聚合(One Server, Multi-Drivers)

与其让 Claude 管理 10 个独立的数据库 Server,不如在 Go 代码层面实现多驱动聚合。这也是我在写这个 SQLite Server 时预留的扩展点。

实现思路:
不要为每种数据库写一个独立的二进制文件,而是写一个 Unified Database Server

  • 动态注册:根据配置文件(如 config.yaml)中开启了哪些数据库,代码里动态执行 s.AddTool
  • 统一接口:AI 面对的是同一个 Server,但工具名可以带上前缀(如 db_sqlite_querydb_mysql_query)。

5.3 终极版:使用 MCP Router(智能网关模式)

这是目前社区最硬核的解决方案。你可以引入一个 MCP Router(路由网关)作为中间层。

  • 原理:Claude 只连接一个“网关 Server”。
  • 按需逻辑:网关 Server 拥有所有子 Server 的索引。只有当 AI 发出类似“我想查一下 MySQL 里的数据”的意图时,网关才动态激活并调用后台的 MySQL 进程。
  • 实战技巧:你可以参考 mcp-router 等开源项目,它们能把几十个 MCP Server 隐藏在一个接口后面。

给文章增加的实操建议(代码层面)

如果你想在 Go 代码里体现这种“按需”的思想,可以优化你的 registerTools 函数:

func registerTools(s *server.MCPServer, enabledDBs []string) {
    for _, dbType := range enabledDBs {
        switch dbType {
        case "sqlite":
            s.AddTool(sqliteListTool, listTablesHandler)
            s.AddTool(sqliteQueryTool, queryHandler)
        case "mysql":
            // 只有配置文件里有 mysql 时,才加载对应工具
            s.AddTool(mysqlQueryTool, mysqlHandler)
        }
    }
}

5.4 总结:为什么要搞“按需加载”?

“MCP 协议解决了 AI 接入世界‘宽度’的问题,而按需加载则是为了解决‘深度’的问题。”

每一个加载进来的工具都会占用 AI 的注意力(Context Window)。优秀的 MCP 实践不应该是把所有的工具一股脑塞给 AI,而是通过合理的命名、清晰的描述、以及必要时的手动/自动隔离,让 AI 在最干净的环境下做出最精准的决策。

六、写在最后:一点实战后的碎碎念

折腾完这个 SQLite MCP Server,最大的感触还是那句话:文档里的“标准化”全是理想,代码里的“调通”才是现实。

MCP 协议虽然看起来只是简单的 JSON-RPC 交互,但在 Go 语言的实战中,选对 SDK 和通信模式能让你少走很多弯路。

  • 关于 SDK: 别迷信“官方”,目前的官方 Go SDK 还是太底层了,写起来心智负担重。mark3labs/mcp-go 这种社区封装在现阶段才是真正的生产力,它让你能把精力放在 SQL 逻辑上,而不是去纠结 JSON 怎么拼。
  • 关于模式: Stdio 模式虽然是 Claude Desktop 的亲儿子,但调试起来真的很痛苦。建议大家在开发阶段先用 HTTP (SSE) 模式跑通逻辑,配合 fmt.Println 把请求看清楚,最后发布时再改回 Stdio。
  • 关于工具治理: 工具不是加得越多越好。AI 有时候也会“间歇性失聪”,给它塞几十个工具只会让它乱选。像我们最后实现的这种“按需加载”或“动态路由”,才是让 AI 真正好用的关键。

写这篇文章的时候,MCP 仍保持着每周多次版本迭代的开发节奏。作为一个开发者,最爽的瞬间莫过于在终端看到 sqlite_query 真正吐出数据,并被 Claude 完美解读的那一刻。这种“把 AI 接入本地私有数据”的快感,确实比单纯的调 API 聊天要强得多。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐