使用Go语言基于MCP协议打造AI选股工具

475 阅读6分钟

在A股市场,选股是每个投资者和量化开发者都绕不开的话题。有没有一种方式,能让我们用自然语言描述选股条件,然后自动获得一份精准的股票清单?答案是肯定的!

我们并不需要自己训练AI模型来理解“金叉”、“市值小于200亿”这些语义,偶然发现东财已经存在支持自然语言的智能选股工具。我们的工作,是用Go语言搭建一个MCP协议服务器,将东财的接口“包装”成AI助手(如Claude Code、VS Code插件等)可以直接调用的工具。

本文将带你用Go语言,基于MCP协议,打造一个“智能”选股工具,并深入剖析其技术原理。

找出MACD金叉且PE小于30的股票

1.png

我要赌狗票:前5天涨停过,今天没有涨停。

2.png

什么是MCP协议?

MCP(Model Context Protocol)是一种面向AI Agent与工具集成的开放协议。它的核心思想是:让AI助手不仅能“聊天”,还能通过标准化的“工具调用”完成实际任务——比如选股、查天气、执行脚本等。

MCP协议基于JSON-RPC 2.0标准,定义了工具的注册、参数描述、调用与响应格式,极大降低了AI前端(如Claude、Cursor、VSCode插件)与后端服务之间的集成门槛。

MCP协议的典型交互流程

MCP协议本质是标准化的JSON-RPC工具调用流程。我们以“智能选股工具”为例,说明其交互过程:

1. 初始化连接

客户端(如AI助手、VS Code插件)首先通过 initialize 方法与MCP服务端建立连接,声明自身能力:

{
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": { "listChanged": true }
    },
    "clientInfo": {
      "name": "test",
      "version": "1.0.0"
    }
  },
  "id": 1
}

服务端响应中会包含所支持的工具列表、参数结构、协议版本等元信息。

2. 调用工具(以选股为例)

客户端通过 tools/call 方法,指定工具名和参数,发起选股请求:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "query_stocks",
    "arguments": {
      "keyword": "MACD水上金叉且流通市值<200亿"
    }
  },
  "id": 2
}

服务端收到后,会调用对应的Go处理器 queryStocksHandler,将自然语言条件转发给东方财富智能选股API,并将结构化结果返回。

3. 响应示例

服务端返回标准JSON-RPC格式的响应,此处我整理为Markdown表格,便于AI理解股票数据:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "## 筛选结果\n|代码|名称|最新价|涨跌幅|换手率|\n|----|----|------|--------|------|\n|601869|长飞光纤|115.28|-4.43%|7.85%|"
      }
    ]
  }
}

了解MCP协议后,我们聊下智能选股是如何落地的。

Go实现:工具注册与API对接

1. 工具注册与描述

我选择 github.com/mark3labs/mcp-go 这个包,通过mcp.NewTool注册了一个名为query_stocks的工具,并详细描述了它的功能和参数:

queryStocks := mcp.NewTool("query_stocks",
    mcp.WithDescription("智能选股:描述你期望的股票(自然语言选股),返回股票的最新交易数据。"),
    mcp.WithString("keyword", mcp.Description("选股条件,例:...")),
)

这里的keyword参数支持自然语言,比如:

  • MACD金叉且PE小于30
  • 最近几天涨停过但今天没有涨停的科技股;不要ST股及不要退市股;不要科创板;不要北交所

2. API接口解析

工具注册后,需要实现对应的处理器(Handler)。在queryStocksHandler中,我们将用户输入的自然语言条件封装为结构体,POST到东方财富接口:

requestBody, _ := json.Marshal(&StockSearchRequest{ ... })
req, _ := http.NewRequest("POST", "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code", bytes.NewBuffer(requestBody))
...
resp, _ := client.Do(req)
body, _ := io.ReadAll(resp.Body)

返回的数据会用gjson解析,提取出股票列表、涨跌幅、换手率等关键信息,最终组装成Markdown表格返回。

3. MCP协议的运行机制

整个流程可概括为:

  • 注册:开发者注册工具,描述其功能与参数。
  • 调用:AI助手或用户通过MCP协议发起工具调用(带参数)。
  • 处理:后端Handler收到请求,执行业务逻辑(如选股),返回结构化结果。
  • 响应:MCP协议统一封装响应,便于AI理解和二次处理。

在本项目中,server.ServeStdio(mcpServer)负责监听MCP协议的请求,并分发到对应的工具Handler。

好的,我知道你们想看代码哈哈!文末有,麻烦点赞谢谢:)

如何本地运行和调试?

  1. 安装依赖:
go mod tidy
  1. 启动服务:
go run main.go
  1. 参考以下配置接入VSCode(Claude Code等都可以)。VSCode配置示例:
{
    "servers": {
        "mcp-server-ai-stock": {
            "command": "go",
            "args": ["run", "main.go"],
            "cwd": "${workspaceFolder}",
            "type": "stdio"
        }
    },
    "inputs": []
}

推荐接入“秘塔搜索MCP”,可以对符合条件的股票进行二次筛选,如盈利情况、财务情况等,更多玩法大家可以评论区交流!

  1. 在AI助手对话框中输入你想要的股票,AI助手会理解你的需求,请求MCP服务器并整理股票数据后返回给你。

总结

MCP协议让AI不再只是“聊天机器人”,而是能真正“动手做事”的智能助手。

用Go语言实现MCP协议的选股工具,可以让AI与金融、数据、自动化等场景深度融合。无论你是AI开发者、量化工程师,还是对智能工具感兴趣的程序员,都值得一试!

本文完整代码如下,欢迎留言交流!

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/spf13/cast"
	"github.com/tidwall/gjson"
)

type StockSearchRequest struct {
	KeyWord                string        `json:"keyWord"`
	PageSize               int           `json:"pageSize"`
	PageNo                 int           `json:"pageNo"`
	Fingerprint            string        `json:"fingerprint"`
	MatchWord              string        `json:"matchWord"`
	ShareToGuba            bool          `json:"shareToGuba"`
	Timestamp              string        `json:"timestamp"`
	RequestID              string        `json:"requestId"`
	RemovedConditionIDList []interface{} `json:"removedConditionIdList"`
	OwnSelectAll           bool          `json:"ownSelectAll"`
	NeedCorrect            bool          `json:"needCorrect"`
	Client                 string        `json:"client"`
	DxInfo                 []interface{} `json:"dxInfo"`
	Biz                    string        `json:"biz"`
	Gids                   []interface{} `json:"gids"`
}

func main() {
	mcpServer := server.NewMCPServer("mcp-server-ai-stock", "1.0.0", server.WithToolCapabilities(false))
	queryStocks := mcp.NewTool("query_stocks",
		mcp.WithDescription("智能选股:描述你期望的股票(自然语言选股),返回股票的最新交易数据。"),
		mcp.WithString("keyword",
			mcp.Description("选股条件,例:\n"+
				"1. 股票或板块名称:宁德时代、比亚迪、新能源、AI、白酒\n"+
				"2. 技术指标:MACD、5日均线、RSI、KDJ\n"+
				"3. 财务条件:ROE>15%、净利润增长率>30%\n"+
				"4. 复合条件:MACD金叉且成交量放大;股价在5日均线上方;不要ST股及不要退市股;不要科创板;不要北交所"),
		),
	)
	mcpServer.AddTool(queryStocks, queryStocksHandler)
	if err := server.ServeStdio(mcpServer); err != nil {
		panic(err)
	}
}

func queryStocksHandler(ctx context.Context, request mcp.CallToolRequest) (res *mcp.CallToolResult, err error) {
	keyword := cast.ToString(cast.ToStringMap(request.Params.Arguments)["keyword"])
	// _, _ = fmt.Fprintln(os.Stderr, ">> "+keyword)
	requestBody, err := json.Marshal(&StockSearchRequest{
		PageNo:                 1,
		PageSize:               50,
		KeyWord:                keyword,
		Fingerprint:            "6d09ac0e7326d9299bb77ac2090dadd9",
		Gids:                   []interface{}{},
		Timestamp:              cast.ToString(time.Now().UnixMilli()),
		RequestID:              "jVZgmtMhiPYtiDb7g2yferYT9aCrY1TA1758537136613",
		NeedCorrect:            true,
		RemovedConditionIDList: []interface{}{},
		DxInfo:                 []interface{}{},
		Biz:                    "web_ai_select_stocks",
		Client:                 "web",
	})
	if err != nil {
		return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("序列化请求失败: %v", err))}}, nil
	}
	client := &http.Client{Timeout: 30 * time.Second}
	req, err := http.NewRequestWithContext(ctx, "POST", "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code", bytes.NewBuffer(requestBody))
	if err != nil {
		return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("创建请求失败: %v", err))}}, nil
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Origin", "https://xuangu.eastmoney.com")
	req.Header.Set("Referer", "https://xuangu.eastmoney.com/")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0")
	resp, err := client.Do(req)
	if err != nil {
		return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("请求失败: %v", err))}}, nil
	}
	defer func() { _ = resp.Body.Close() }()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("读取响应失败: %v", err))}}, nil
	}
	g := gjson.ParseBytes(body)
	if g.Get("code").Int() != 100 {
		return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(fmt.Sprintf("响应非100: %s", string(body)))}}, nil
	}
	if g.Get("data.result.total").Int() <= 0 {
		return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent("未找到符合条件的股票")}}, nil
	}
	content := parseSearchCodeResponse(g)
	// _, _ = fmt.Fprintln(os.Stderr, content)
	return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(content)}}, nil
}

func parseSearchCodeResponse(g gjson.Result) string {
	var (
		columns []string
		b       strings.Builder
	)
	_, _ = b.WriteString("## 筛选到的股票数据\n|")
	g.Get("data.result.columns").ForEach(func(_, o gjson.Result) bool {
		columns = append(columns, o.Get("key").String())                             // 英文字段
		_, _ = b.WriteString(o.Get("title").String() + o.Get("unit").String() + "|") // 中文表头
		return true
	})
	g.Get("data.result.dataList").ForEach(func(_, o gjson.Result) bool {
		_, _ = b.WriteString("\n|")
		for _, column := range columns {
			_, _ = b.WriteString(strings.ReplaceAll(o.Get(column).String(), "|", ",") + "|")
		}
		return true
	})
	return b.String()
}