基于数据表搭建MCP

本节内容可以参考CaseStudy目录下的文件"MCPServer.foxdb"

在上一节中,我们通过硬编码的方式,手写了两个工具的 JSON 定义。虽然逻辑清晰,但你可能已经发现了一个痛点:JSON 的嵌套结构太长了,手写不仅容易漏掉括号,对于非专业用户来说也极难维护。

既然我们在之前的章节学会了用数据表管理 AI 工具,那么我们同样可以用数据表来管理 MCP 工具。因为 MCP 的 inputSchema 本质上就是标准的 JSON Schema,完全可以用字段来描述。

本节的任务:把 MCP 服务端的工具定义,从代码里彻底抽离到数据表中,实现“零代码”动态生成 MCP 菜单。

本节的MCP服务提供了六个工具,分别用于列出所有表、获取指定表结构、条件计算、分组统计、增加行、获取今天日期。


首先要创建两个表,MCP_Tools 和 MCP_Parameters,分别用于输入工具和工具参数,二者通过 ToolID 关联。

MCP_Tools 表结构:

列名 说明
ToolID 工具ID,整数型
ToolName 工具名,字符串型
Description 工具描述,备注型

MCP_Parameters表结构:

列名 说明
ToolID 工具ID,整数型
SortOrder 参数顺序,整数型
ParameterName 参数名称,字符串型
ParameterType 参数类型,字符串型
IsRequired 是否为必填参数,逻辑型
ArrayItemType 如果参数类型为数组(array),可用此列指定元素类型,字符串型
EnumValues 候选值,用逗号分割,字符串型
Description 参数描述,备注型

AfterOpenProject事件和HttpRequest事件的代码不变,变化的是自定义函数HandleMCP的代码。

自定义函数HandleMCP的代码如下,代码比较长,主要是因为包括了6个工具的执行代码:

Dim e As RequestEventArgs = Args(0)
If
String.IsNullOrEmpty(e.PlainText) Then
   
Dim emptyErr As New JObject From {{"jsonrpc", "2.0"}, {"id", Nothing}, {"error", New JObject From {{"code", - 32700}, {"message", "Empty request"}}}}
    e.WriteString(emptyErr.ToString(),
"application/json")
   
Return Nothing
End
If

Dim
req As JObject = JObject.Parse(e.PlainText)
Dim
method As String = If(req("method") IsNot Nothing, req("method").ToString(), "")
Dim
id As JToken = req("id")
Dim
resp As New JObject From {{"jsonrpc", "2.0"}, {"id", id}}

Try

   
Select Case method
       
Case "initialize"
            resp(
"result") = New JObject From {
                {
"protocolVersion", "2025-11-25"},
                {
"capabilities", New JObject From {{"tools", New JObject()}}},
                {
"serverInfo", New JObject From {{"name", "foxDataMcp"}, {"version", "1.0"}}}
            }
       
Case "notifications/initialized"
            e.StatusCode = 202
            e.WriteString(
"", "application/json")
           
Return Nothing
       
Case "tools/list" '动态读取数据表生成 MCP 菜单
           
Dim builder As New AIToolBuilder()
           
For Each tr As DataRow In DataTables("MCP_Tools").DataRows
               
Dim tool As AITool = builder.AddFunction(tr("ToolName"), tr("Description"))
               
For Each pr As DataRow In DataTables("MCP_Parameters").Select("ToolID = " & tr("ToolID"), "SortOrder")
                   
Dim EnumValues() As String = Nothing
                   
If pr.IsNull("EnumValues") = False Then
                        EnumValues = pr(
"EnumValues").Split({","c, ","c})
                   
End If
                    tool.AddParameter(pr(
"ParameterName"), pr("ParameterType"), pr("Description"), pr("IsRequired"), pr("ArrayItemType"), EnumValues)
               
Next
           
Next
            resp(
"result") = New JObject From {{"tools", builder.BuildMCP()}} '调用BuildMCP方法生成MCP格式的工具定义
       
Case "tools/call" '干活:执行工具
           
Dim params As JObject = req("params")
           
Dim toolName As String = params("name").ToString()
           
Dim toolArgs As JObject = CType(params("arguments"), JObject)
           
Dim txt As String = ""
           
Dim isError As Boolean = False
           
Try
               
Select Case toolName
                   
Case "compute_table" ' 条件计算
                       
Dim tableName As String = toolArgs("table_name").ToString()
                       
If DataTables.Contains(tableName) = False Then
                            txt =
"{""error"":""表 '" & tableName & "' 不存在""}"
                            isError =
True
                       
Else
                           
Dim expression As String = toolArgs("expression").ToString()
                           
Dim filter As String = ""
                           
If toolArgs.ContainsKey("filter") Then filter = toolArgs("filter").ToString()
                           
Dim dt As DataTable = DataTables(tableName)
                           
Dim result As Object = dt.Compute(expression, filter)
                            txt =
If(result Is Nothing, "", result.ToString())
                       
End If
                   
Case "add_row" ' 增加行
                       
Dim tableName As String = toolArgs("table_name").ToString()
                       
If DataTables.Contains(tableName) = False Then
                            txt =
"{""error"":""表 '" & tableName & "' 不存在""}"
                            isError =
True
                       
Else
                           
Dim rowData As JObject = toolArgs("row_data")
                           
Dim dt As DataTable = DataTables(tableName)
                           
Dim dr As DataRow = dt.AddNew()
                           
For Each prop As JProperty In rowData.Properties()
                               
Dim colName As String = prop.Name
                               
Dim colValue As JToken = prop.Value
                               
If dt.DataCols.Contains(colName) Then
                                    dr(colName) = colValue.ToString()
                               
End If
                           
Next
                            txt =
"{""message"":""行已成功添加到表 '" & tableName & "',请手动保存数据。""}"
                       
End If
                   
Case "get_today_date"
                        txt = Format(
Date.Now, "yyyy-MM-dd")
                   
Case "list_tables"
                       
Dim nms As New JArray
                       
For Each dt As DataTable In DataTables
                            nms.Add(dt.Name)
                       
Next
                        txt = nms.ToString()
                   
Case "get_table_schema"
                       
Dim tableName As String = toolArgs("table_name").ToString()
                       
If DataTables.Contains(tableName) = False Then
                            txt =
"{""error"":""表 '" & tableName & "' 不存在""}"
                            isError =
True
                       
Else
                           
Dim dt As DataTable = DataTables(tableName)
                           
Dim columns As New JArray
                           
For Each dc As DataCol In dt.DataCols
                               
Dim column As New JObject
                                column(
"name") = dc.Name
                                column(
"type") = dc.DataType.Name
                               
If dc.Expression > "" Then column("expression") = dc.Expression
                                columns.Add(column)
                           
Next
                            txt = columns.ToString()
                       
End If
                   
Case "group_statistics" ' 分组统计
                       
Dim tableName As String = toolArgs("table_name").ToString()
                       
If Not DataTables.Contains(tableName) Then
                            txt =
"{""error"":""表 '" & tableName & "' 不存在""}"
                            isError =
True
                       
Else
                           
Dim groupCols As JArray = toolArgs("group_columns")
                           
Dim aggCols As JArray = toolArgs("aggregates")
                           
Dim dt As DataTable = DataTables(tableName)
                           
Dim gb As New GroupTableBuilder("临时统计表", dt) 
                           
For Each gcol As String In groupCols
                               
Dim parts() As String = gcol.Split("|"c)
                               
Dim colName As String = parts(0)
                               
If dt.DataCols(colName).IsDate Then
                                   
Dim groupMode As DateGroupEnum = DateGroupEnum.Month
                                   
If parts.Length > 1 Then [Enum].TryParse(Of DateGroupEnum)(parts(1), True, groupMode)
                                   
Dim aliasName As String = If(parts.Length > 2, parts(2), "")
                                    gb.Groups.AddDef(colName, groupMode, aliasName)
                               
Else
                                   
If parts.Length = 1 Then
                                        gb.Groups.AddDef(colName)
                                   
ElseIf parts.Length > 1 Then
                                        gb.Groups.AddDef(colName, parts(1))
                                   
End If
                               
End If
                           
Next
                           
For Each aggItem As String In aggCols
                               
Dim parts() As String = aggItem.Split("|"c)
                               
Dim colName As String = parts(0)
                               
Dim aggMethod As String = If(parts.Length > 1, parts(1).ToLower(), "sum")
                               
Dim aliasName As String = If(parts.Length > 2, parts(2), "")
                               
Dim aggType As AggregateEnum = AggregateEnum.Sum
                               
If aggMethod > "" Then [Enum].TryParse(Of AggregateEnum)(aggMethod, True, aggType)
                                gb.Totals.AddDef(colName, aggType, aliasName)
                           
Next
                           
If toolArgs.ContainsKey("filter") Then gb.Filter = toolArgs("filter").ToString()
                           
If toolArgs.ContainsKey("same_period_growth") Then gb.SamePeriodGrowth = toolArgs("same_period_growth").ToObject(Of Boolean)()
                           
If toolArgs.ContainsKey("circle_growth") Then gb.CircleGrowth = toolArgs("circle_growth").ToObject(Of Boolean)()
                           
If toolArgs.ContainsKey("grand_proportion") Then gb.GrandProportion = toolArgs("grand_proportion").ToObject(Of Boolean)()
                           
If toolArgs.ContainsKey("group_proportion") Then gb.GroupProportion = toolArgs("group_proportion").ToObject(Of Boolean)()
                           
Dim resultTable As DataTable = gb.Build(True)
                           
Dim resultJson As New JObject
                           
Dim colNames As New JArray
                           
For Each dc As DataCol In resultTable.DataCols
                                colNames.Add(dc.Caption)
                           
Next
                            resultJson(
"columns") = colNames
                           
Dim rows As New JArray
                           
Dim maxRows As Integer = Math.Min(resultTable.dataRows.Count, 1000)
                           
For r As Integer = 0 To maxRows - 1
                               
Dim dr As DataRow = resultTable.DataRows(r)
                               
Dim rowArray As New JArray
                               
For Each dc As DataCol In resultTable.DataCols
                                   
If dr.IsNull(dc) Then
                                        rowArray.Add(
Nothing)
                                   
Else
                                        rowArray.Add(dr(dc.Name))
                                   
End If
                               
Next
                                rows.Add(rowArray)
                           
Next
                            resultJson(
"data") = rows
                            txt = resultJson.ToString()
                       
End If
                   
Case Else
                        txt =
"未知工具:" & toolName
                        isError =
True
               
End Select
           
Catch ex As Exception
               
' 捕获具体工具执行时的报错
                txt =
"{""error"":""" & ex.Message & """}"
                isError =
True
           
End Try
           
' 将执行结果按照 MCP 标准格式包好
            resp(
"result") = New JObject From {
                {
"isError", isError},
                {
"content", New JArray From {New JObject From {{"type", "text"}, {"text", txt}}}}
            }
   
End Select
Catch
ex As Exception
   
' 全局兜底
    resp =
New JObject From {{"jsonrpc", "2.0"}, {"id", id}, {"error", New JObject From {{"code", - 32603}, {"message", ex.Message}}}}
End
Try
'
输出最终响应
e.WriteString(resp.ToString(),
"application/json")

客户端测试代码

现在你可以在客户端项目通过LLMCaller调用这个MCP服务,代码和上一节完全一样,只是系统提示词和提交的问题不同而已。

以下代码请在另一个项目的命令窗口执行:

Dim deepseekKey As String = "sk-a734b83536da45e9aedc3e42605bfb3a" '你的DeepSeek密钥
Dim
caller As New LLMCaller("https://api.deepseek.com/chat/completions", "deepseek-chat", deepseekKey)
caller.SystemPrompt =
"你是一个数据统计分析专家"
Dim
mcpUrl As String = "http://127.0.0.1:18688/mcp"
Dim
hc As New HttpClient(mcpUrl)
hc.SkipError =
True
hc.ContentType =
"application/json"
hc.Headers.Add(
"Accept", "application/json, text/event-stream")
' 1.
拉取我们自己的工具列表
hc.Content =
"{""jsonrpc"":""2.0"",""id"":1,""method"":""tools/list"",""params"":{}}"
Dim
listJson As JObject = JsonConvert.DeserializeObject(hc.GetData())
Dim
mcpTools As JArray = listJson("result")("tools")
caller.AddMCPTools(mcptools)
'将MCP工具注册到LLMCaller
' 2.
设置工具执行器(转发给我们的私有 MCP)
caller.ToolExecutor =

Function
(ttoolName As String, targs As JObject) As String
   
Dim callReq As New JObject From {
        {
"jsonrpc", "2.0"}, {"id", Rand.NextString(10)}, {"method", "tools/call"},
        {
"params", New JObject From {{"name", ttoolName}, {"arguments", targs}}}
    }
    hc = hc.Clone
    hc.Content = callReq.ToString()
   
Dim respJson As JObject = JsonConvert.DeserializeObject(hc.GetData())
   
Return respJson("result")("content")(0)("text").ToString()
End
Function
' 3.
让 AI 发起提问
caller.UserMessage =
"按年统计产品的销量,并给出同比增长率"
If
caller.GetResult() Then
    MessageBoxA.Show(caller.Result)

Else

    MessageBoxA.Show(caller.Error)

End
If

将MCP服务连接到智能体软件

同样也可以连接到Trae和WorkBuddy等智能体软件,配置为:

{
    "mcpServers": {
        "foxDataMcp": {
            "url": "http://127.0.0.1:18688/mcp"
        }
    }
}



本页地址:http://www.foxtable.com/webhelp/topics/7301.htm