基于数据表搭建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"
}
}
}
