18 · MCP 服务器
相关:第 15 章 自动化 API 总览 · 第 16 章 自动化 API 方法速查 · docs/MCP服务器-外部AI连接说明.md
这一章是什么
MCP(Model Context Protocol)是一套"让 AI 连上你的工具"的标准协议。
Hmi.Mcp.Host 就是一个 MCP 服务器 —— 跑起来之后,Claude、Cursor 这类 AI 就能直接读写你的工程。
什么时候你会需要它:
- 你想让 AI 帮你批量改点表、批量建画面,而不是自己一个个点
- 你想用自然语言改工程:"把所有温度点的报警上限调成 120"
- 你要给公司内部的 AI 工具开一个组态入口
它是怎么做到的:MCP 工具全部是自动化 API 的薄包装 ——
每个工具就是调一下 IProjectAutomation 上的对应方法,把结果转成 JSON 回给 AI。
所以:第 15、16 章讲的能力,MCP 全都有;第 16 章没讲的能力,MCP 也没有。
一、先搞明白:它能做什么、不能做什么
能做
| 感知(只读) | 看工程里有什么 —— 画面、点、通道、协议、配方、报警配置… |
| 组态(可写) | 改工程文件 —— 加删点、建画面、摆元素、配报警、配历史、写脚本… |
不能做
| 为什么 | |
|---|---|
| 写实时值 / 下发 PLC | 有物理后果。这条边界贯穿整个软件 |
| 启停设备 | 同上 |
| 改运行态 | 只碰磁盘上的工程文件,不碰内存里的运行数据 |
这是设计上刻意不开的,不是"还没做"。MCP 工具清单里一个运行态写值工具都没有。
想理解为什么这么设计:因为 AI 会犯错。组态改了可以撤销、可以重新打开工程; 而给一台运行中的锅炉下发错误设定值,可能直接出事故。
也不提供的东西
| 能力 | 状态 |
|---|---|
prompts(提示词模板) |
不暴露 —— 请求会回错误码 -32601(方法不存在) |
resources(资源) |
不暴露 —— 同上 |
instructions(服务器说明) |
不带(为 None) |
提示词从哪来? —— 全部走每个工具自己的 description,包括:
- 工具级
[Description]—— 说这个工具干什么 - 参数级
[Description]—— 说每个参数填什么
覆盖率 100%,而且有测试钉死(少一个就红)。为什么这么较真: 这两样东西就是给 AI 的提示词。缺一个,AI 就不知道该填什么,只能瞎猜。
二、exe 在哪
MCP 服务器是个独立 exe,随发行包一起提供,不需要编译:
<安装目录>\
组态编辑器.exe ← 主程序,双击进组态开发环境
mcp\Hmi.Mcp.Host.exe ← MCP 服务器(就是它)
project\ ← 随包自带的示例工程(含 project.json)
它在主程序旁边的
mcp子目录里,是一个独立进程(与主程序不是同一个进程,也不共用运行时)。 配置时路径别写成主程序那个 exe。
三、命令行参数
Hmi.Mcp.Host.exe <工程目录> [选项]
| 参数 | 默认值 | 意思 |
|---|---|---|
<工程目录> |
— | 第一个不以 -- 开头的参数就是工程目录 |
--http [<host> <port>] |
127.0.0.1 8765 |
用 http 模式(不写就是 stdio 模式) |
--token <口令> |
无 | http 模式的访问口令 |
--read-only |
关 | 只读模式:只暴露感知类工具 |
--tools a,b,c |
无 | 只暴露这几个工具(逗号分隔) |
也可以用环境变量 HMI_PROJECT_DIR 指定工程目录。
⚠ 参数解析很宽松,几个要注意的
| 情况 | 行为 |
|---|---|
没有 --help |
不存在这个参数 |
不认识的 --xxx |
静默忽略(不报错、不提示) |
| 参数缺值 / 端口填了非数字 | 宽松回落到默认值(不报错) |
--stdio |
⚠ 不是真参数 —— 传了也被忽略。默认就是 stdio |
⚠ 最后一条容易误导:
tools/mcp-probe.mjs里传了--stdio,看着像是"显式选了 stdio 模式", 其实是被忽略的,生效的是默认值。写配置时别以为必须传它。
启动失败的返回码
| 情况 | 返回码 |
|---|---|
| 没给工程目录 | 2(并往 stderr 打用法) |
| 目录不存在 | 2 |
| 打开工程失败 | 2 |
| 工具被过滤成空集 | 2 |
四、两种连接方式
4.1 stdio 模式(默认)
服务器不说话的时候不占任何东西,通过标准输入输出和客户端通信。
Hmi.Mcp.Host.exe "D:\我的工程"
适合:Claude Desktop、Cursor 这类自己管子进程的客户端。 客户端拉起这个 exe,往它 stdin 写请求、从 stdout 读回答。
4.2 http 模式(常驻)
监听一个端口,多个客户端可以连同一个实例。
Hmi.Mcp.Host.exe "D:\我的工程" --http 127.0.0.1 8765
| 地址 | http://127.0.0.1:8765/mcp |
| 鉴权 | 设了 --token 才校验 |
鉴权方式(任选一种):
Authorization: Bearer <token>
X-Api-Key: <token>
不匹配回 401。没设 token 就不校验 —— 所以本机用可以不管,
但如果要监听 0.0.0.0(别人也能连),一定要设 token。
适合:多个工具连同一个实例、或只支持 http 的客户端。
4.3 在组态器里启停
组态器界面里「工具 → MCP 服务器」面板可以一键启停,不用敲命令行。
配置存在 %APPDATA%\FangLikong\mcp-server.json,字段和命令行参数一一对应。
详见第 17 章第七节。
五、能用的工具:共 134 个
感知 57 个、其余 77 个(组态 + 删除)。
这几个数字和下面两张表的每一行都由测试直读本章核对 (
tests/Hmi.Mcp.Host.Tests/McpToolShapeTests.cs的Tool_count_in_the_ai_facing_doc_matches_reality+Manual_counts_match_reality+Manual_section_inventories_match_reality)。 改工具清单必须同步改这里,否则测试红。两张表是生成的:
python tools/gen-mcp-manual.py --write读src/Hmi.Mcp.Host/ProjectMcpTools.cs的每个[Description]重建(已有行的人工文案保留), 分组按「动词 + 名词」规则推。别手改行 —— 手改的部分下次生成就没了; 真要改文案,改[Description]。
5.1 感知类(只读)
看工程整体
| 工具 | 干什么 |
|---|---|
get_project |
读工程元信息(名/版本/启动画面/说明) |
get_project_tree |
工程树总览:画面→元素(名/类型)、点表规模、通道→设备、配方、报警/历史条数、报表 |
get_theme |
读取工程级语义色板 |
validate_project |
整份工程校验(点 Id 连续性、重名、引用有效性) |
画面与元素
| 工具 | 干什么 |
|---|---|
describe_element_props |
取某元素类型的可写属性清单(改属性前必须先调它) |
get_element |
读单个元素的完整模型(类型/全部属性/绑定/事件) |
get_screen |
取一个画面的完整模型 |
list_element_types |
列所有可用元件类型 |
list_elements |
列画面里的元素 |
list_screens |
列所有画面 |
list_symbols |
列现成工业符号库 |
validate_element |
干跑校验:建元素前先检查一组属性能否落在某元素类型上(可写属性名/值可转/闭集枚举合法) |
点表与通道
| 工具 | 干什么 |
|---|---|
describe_protocol |
描述一个协议支持哪些连接方式及参数 |
get_tag |
取一个点的定义 |
list_channels |
列所有通道/设备 |
list_global_variables |
列全局内部变量 |
list_protocols |
列可用协议 + 地址语法提示 |
list_screen_variables |
列某画面的窗口变量 |
list_tag_types |
列可选点类型 + 每种类型哪些字段有意义 |
list_tags |
列所有点位定义 |
read_tag_once |
单次读一个点的当前值(只读一次,不订阅、不写值) |
test_channel |
连通测试一个通道(只读探测,绝不下发写) |
脚本
| 工具 | 干什么 |
|---|---|
build_project |
编译校验整份工程的所有脚本 |
check_script |
只编不跑一段脚本(查语法错) |
get_element_events |
读元素事件脚本 |
get_module |
取一个模块的完整内容 |
get_project_scripts |
读工程级脚本 |
get_screen_scripts |
读画面脚本 |
list_function_catalog |
列内置脚本函数目录(15 库 / 176 函数) |
list_modules |
列脚本模块 |
list_project_periodics |
列后台定时脚本(含触发方式 At/IntervalMs 与中文描述 Trigger) |
配方 / 报警 / 历史 / 报表
| 工具 | 干什么 |
|---|---|
get_alarm |
取一个点的报警配置 |
get_history_rule |
取一个点的历史规则 |
get_recipe |
取配方完整内容 |
get_report |
取报表定义 |
list_alarms |
列所有点的报警配置 |
list_history_rules |
列所有点的历史规则 |
list_recipes |
列配方 |
list_reports |
列报表定义 |
query_report |
按已定义的报表出一张真实表格(AI 辅助调试用):Historian 源查历史库聚合 |
多语言 / 皮肤
| 工具 | 干什么 |
|---|---|
get_i18n |
读整份多语言表 |
get_translation |
查某源文本在某语言下的译文 |
list_button_skins |
列自定义按钮皮肤 |
list_languages |
列语言 + 译条数 |
list_style_presets |
列内置外观预设的 Id/中文名/分类 |
转发 / 通知 / 启动 / 发布
| 工具 | 干什么 |
|---|---|
get_forward |
取一条转发规则的完整定义 |
get_notify_config |
读通知配置(SMTP/收件人/触发级别等) |
get_publish_profile |
读交付发布配置(exe 文件名/产品信息/图标/版本;含留空回退值) |
get_startup_config |
读启动配置:启动画面 + 启动/退出时要调的全局子程序清单 |
list_forwards |
列所有北向转发规则 |
数据库
| 工具 | 干什么 |
|---|---|
list_db_connections |
列工程级数据库连接 + 可用驱动清单 |
导出类(只读,但有输出)
| 工具 | 干什么 |
|---|---|
export_project |
导出 .hmiproj 归档 |
export_recipe |
导出配方为 JSON 文本 |
export_tags |
导出整张点表为 JSON 文本 |
export_translations_csv |
把多语言表导出成 CSV 文本(第一列原文,其余每语言一列) |
pack_project |
封成加密运行封包 .hmipkg |
类型结构
| 工具 | 干什么 |
|---|---|
describe_type_schema |
查一个嵌套/复杂类型的字段结构(名/类型/枚举合法值/默认值/是否必填) |
为什么"导出类"和
build_project也算感知:它们不改工程模型 ——export_*/pack_project只是把现有内容导出,build_project只编不跑、回一份诊断。判据的唯一真源是src/Hmi.Automation/SensingTools.cs:前缀get_list_describe_query_read_validate_check_, 外加显式名单里的export_projectexport_tagsexport_recipeexport_translations_csvpack_projectbuild_projecttest_channelread_tag_oncequery_report。 配readOnly: true时只有感知类会被暴露出去。
5.2 组态类(可写)
画面
| 工具 | 干什么 |
|---|---|
create_screen |
新建画面 |
delete_screen |
删画面 |
duplicate_screen |
复制整张画面(深拷贝控件树+全部属性/绑定/事件) |
rename_screen |
画面改名 |
set_screen_props |
改画面属性(尺寸/背景/网格) |
元素
| 工具 | 干什么 |
|---|---|
add_element |
加元素 |
align_elements |
把多个元素对齐到统一基准(以选中元素的外包围盒为参照) |
apply_style |
套预置样式 |
distribute_elements |
把多个元素沿某方向均匀分布(相邻元素边到边的间隙相等,首尾位置不动) |
duplicate_element |
复制元素(深拷贝,含全部属性/绑定/事件),按偏移摆放 |
group_elements |
把多个元素成组(扁平分组,同 GroupId 即一组):校验都在(缺则整批不改) |
move_element |
改几何(位置/大小/旋转) |
remove_binding |
删绑定 |
remove_element |
删元素 |
rename_element |
元素改名,保留其绑定/事件/属性(优于「删了重建」——重建会丢绑定和事件) |
reorder_element |
调图层次序 |
set_binding |
设/改绑定 |
set_element_props |
改非几何属性(文本/颜色/字号/动作) |
set_elements_props |
批量改多个元素的同一组属性 |
ungroup_elements |
解组:传入任一成员即解散它所在的整组(清空该组所有成员的 GroupId) |
点 / 变量
| 工具 | 干什么 |
|---|---|
add_internal_variable |
加全局内部变量 |
add_screen_variable |
加窗口变量 |
add_tag |
加点 |
import_tags |
从 JSON 文本(单个点或点数组)导入点表并按点名合并(同名覆盖) |
remove_tag |
删点 |
update_tag |
改点定义 |
通道 / 设备
| 工具 | 干什么 |
|---|---|
add_channel |
加通道 |
add_device |
加设备 |
remove_channel |
删通道 |
remove_device |
删设备 |
update_channel |
改通道 |
update_device |
改设备 |
脚本
| 工具 | 干什么 |
|---|---|
add_module |
新建脚本模块 |
delete_module |
删一个脚本模块(引用它的脚本编译时会报未定义,删前先确认没被引用) |
remove_project_periodic |
删后台定时脚本 |
save_module |
新建/整体保存一个脚本模块(整个工程共享的函数库) |
set_element_events |
整体保存一个元素的事件脚本集(Handlers:事件名→脚本,如按钮 Click) |
set_project_periodic |
加/改后台定时脚本(按间隔 intervalMs,或按时刻 at 如 *:00) |
set_project_scripts |
整体保存工程级脚本 |
set_screen_scripts |
整体保存一个画面的脚本(OnEnter/OnExit/Periodic) |
配方
| 工具 | 干什么 |
|---|---|
create_recipe |
建空配方 |
delete_recipe |
删配方 |
import_recipes |
从 JSON 导入配方 |
remove_recipe_item |
删配方项 |
set_recipe_element_item |
设配方里的编辑框目标 |
set_recipe_item |
设配方里的点目标 |
set_recipe_remark |
设备注 |
报警 / 历史 / 报表
| 工具 | 干什么 |
|---|---|
remove_alarm |
删报警配置 |
remove_history_rule |
删历史规则 |
remove_report |
删报表定义 |
set_alarm |
设/改点的报警配置 |
set_alarms_bulk |
批量设/改报警配置(一次多条,省逐条 100 次往返;100 台机组配同类报警就用它) |
set_history_rule |
设/改点的历史规则 |
set_history_rules_bulk |
批量设/改历史记录规则(一次多条) |
set_report |
设/改报表定义 |
多语言
| 工具 | 干什么 |
|---|---|
add_language |
加目标语言 |
extract_translations |
从画面抽取可翻译文本 |
import_translations_csv |
从 CSV 文本导入译文(表头首列为原文、其余列为语言代码;按原文匹配填充) |
remove_language |
删一个目标语言:从目标语言列表移除,并清掉每条译条里这门语言的译文(源文本保留) |
remove_translation |
删译文(与 set_translation 对称) |
set_default_language |
设默认(源)语言代码,如 zh-CN / en-US |
set_i18n |
整份覆盖多语言表 |
set_translation |
设一条译文 |
皮肤
| 工具 | 干什么 |
|---|---|
add_button_skin |
加自定义按钮皮肤 |
remove_button_skin |
删皮肤 |
update_button_skin |
改皮肤 |
转发 / 通知 / 启动 / 发布 / 工程信息
| 工具 | 干什么 |
|---|---|
remove_forward |
删一条转发规则 |
set_forward |
设/改一条北向转发规则(同名整体覆盖) |
set_notify_config |
写通知配置(整份替换) |
set_project_info |
写工程元信息:工程名 name / 说明 description |
set_publish_profile |
写交付发布配置 |
set_startup_config |
写启动配置 |
set_theme_color |
新增或修改工程级语义色角色 |
数据库
| 工具 | 干什么 |
|---|---|
delete_db_connection |
删除一个工程级数据库连接(不动物理数据文件——那是用户数据) |
set_db_connection |
新增或修改一个工程级数据库连接(按连接名匹配,忽略大小写) |
持久化
| 工具 | 干什么 |
|---|---|
import_project |
从归档还原工程 |
save_project |
校验 + 落盘 |
⚠ 组态完必须调 save_project
add_tag / create_screen 这些只改内存,不落盘。
最后一定要调 save_project,否则改了半天磁盘上没变。
这一点对 AI 尤其重要 —— 它可能会觉得"我调了 add_tag 就是加好了"。 工具级
description里写了这件事,但要提醒用 AI 的人注意。
六、工具过滤
McpToolFilter 决定哪些工具暴露出去:
public static bool IsSensing(string name); // 是不是感知类
public static IReadOnlyCollection<string> Build(
ProjectAutomation automation, bool readOnly, List<string>? whitelist);
| 参数 | 效果 |
|---|---|
readOnly = true |
只暴露感知类 |
whitelist |
只暴露清单里的 |
| 两个都给了 | 是"与"关系 —— 既要在白名单里,又要符合 readOnly |
readOnly和whitelist是与关系,不是或 —— 别指望"白名单里的写工具能绕过 readOnly"。只有 http 模式用过滤。stdio 模式全量暴露。 所以
--read-only只在 http 模式下有意义。
七、怎么在客户端里配上
Claude Desktop
改 claude_desktop_config.json:
{
"mcpServers": {
"hmi": {
"command": "C:\\app\\mcp\\Hmi.Mcp.Host.exe",
"args": ["D:\\我的工程"]
}
}
}
Cursor
改 ~/.cursor/mcp.json,格式同上。
Claude Code
claude mcp add hmi -- "C:\app\mcp\Hmi.Mcp.Host.exe" "D:\我的工程"
⚠ 改完要完全重启客户端
不是关窗口 —— 要真的退出进程。很多客户端只在启动时读一次配置。
八、自检:先确认它真的能用
有个探针脚本:
node tools/mcp-probe.mjs
| 用法 | 说明 |
|---|---|
node tools/mcp-probe.mjs |
默认用 samples/DemoProject |
node tools/mcp-probe.mjs <工程目录> |
指定工程 |
node tools/mcp-probe.mjs --json |
输出 JSON |
它做什么:
- 自己
spawn这个 exe(自动在 Debug / Release 里找) - 走一遍协议:
initialize→notifications/initialized→tools/list→prompts/list→resources/list - 统计工具数、参数数、缺描述项
- 验证
prompts/resources是不是真的回-32601
退出码:有工具缺描述、或有参数缺描述 → 非 0。所以可以当 CI 闸门用。
配客户端之前先跑一遍这个 —— 如果探针都跑不通,说明 exe 没编译好或工程目录不对, 那客户端里配了也是白配。
九、一个完整的使用例子
场景:让 AI 帮我把所有温度点的报警上限统一改成 120。
你(在 Claude Desktop 里)说:
帮我把这个工程里所有名字带"温度"的点,报警上限 Hi 改成 120、HiHi 改成 130, 回差 2,延时 3 秒。改完保存。
AI 大概会这么做:
① list_tags → 拿到所有点,筛出名字含"温度"的
② 对每个点:
get_alarm("烤箱.温度") → 先看现有配置(了解结构)
set_alarm(tagName:"烤箱.温度", deadband:2, delaySeconds:3,
hi: {Limit:120, Level:"Alarm"},
hiHi: {Limit:130, Level:"Critical"})
③ save_project → 落盘
④ 回报:改了 12 个点
⚠
set_alarm的参数形状:hi/hiHi/lo/loLo收的是对象 ({Limit, Level, Message}),不是数字;deadband是回差、delaySeconds是秒。 这是 MCP 层的入参形态,和AlarmConfigModel的字段名一致(hiHi→HiHi)。 详见第 16 章第 8 组。注意
get_alarm那一步:如果这个点本来没配报警,get_alarm会返回失败 —— 这是正常的,直接set_alarm就行。
如果 AI 改元素属性改错了,大概率是它没先调 describe_element_props。
提醒它:"先查一下这个元素有哪些可写属性"。
十、这一章的坑
| # | 坑 | 怎么避 |
|---|---|---|
| 1 | 改完忘了 save_project |
内存改了磁盘没变。AI 最容易漏这步 |
| 2 | 以为 --stdio 是真参数 |
它被静默忽略,默认就是 stdio |
| 3 | 以为有 --help |
没有。参数写错了也不报错,静默回落默认值 |
| 4 | --read-only 只在 http 模式生效 |
stdio 模式全量暴露 |
| 5 | readOnly + whitelist 想成"或" |
是"与" |
| 6 | http 监听 0.0.0.0 又不设 token |
谁都能改你的工程 |
| 7 | 改完客户端没重启 | 要真的退出进程 |
| 8 | set_element_props 属性名瞎猜 |
先 describe_element_props |
| 9 | 指望它写实时值 | 没有这类工具,设计上不开 |
| 10 | 点 Id 跳号导致 save_project 失败 |
别自己指定 Id |
十一、和自动化 API 的对应关系
MCP 工具就是自动化 API 的包装。对照着看:
| MCP 工具名 | 对应 IProjectAutomation 方法 |
|---|---|
get_project |
GetProject() |
add_tag |
AddTag(tag) |
set_alarm |
SetAlarm(config) |
save_project |
Save() |
validate_project |
Validate() |
| … | … |
唯一的不同:MCP 工具名用 snake_case(add_tag),
自动化 API 方法名用 PascalCase(AddTag)。
所以自动化 API 加一个方法,想让它能通过 MCP 用,就要在
ProjectMcpTools里加一个对应的工具方法, 并且工具级和参数级的[Description]都要写 —— 不然测试会红。
本章内容以
src/Hmi.Mcp.Host/与docs/MCP服务器-外部AI连接说明.md为准。 工具计数被测试直读,改工具必须同步改文档。