从 LINQPad 到 MCP:打造 CRM 智能助手的实战记录
2026 年 7 月 30 日 · 约 15 分钟阅读
一、缘起
日常工作涉及 Dynamics 365 CRM 的开发和运维,经常需要查询实体元数据、管理解决方案、迁移组件。这些操作传统上依赖 LINQPad 脚本------一个强大的 .NET 交互式编程工具。但随着 AI 编程助手(如 OpenCode)的普及,一个想法自然浮现:能不能让 AI 直接帮我查 CRM 实体、管理解决方案?
答案是肯定的。MCP(Model Context Protocol)应运而生,它定义了 AI 客户端与工具服务器之间的通信标准。我们只需要把 LINQPad 脚本包装成 MCP 服务器,AI 就能通过标准接口调用这些能力。
📌 什么是 MCP?
MCP(Model Context Protocol)是一种开放协议,允许 AI 模型通过标准化的接口与外部工具和数据源交互。它类似于 AI 世界的 USB 协议------定义好接口,工具就能即插即用。OpenCode、Claude Desktop 等客户端都支持 MCP。
二、从 LINQPad 脚本开始
在开始 MCP 之前,我们已经积累了大量 LINQPad 脚本。其中最核心的是 A生成数据字典.linq,它能够:
- 连接 CRM 服务(通过
Dynamic365.GetService_YFT_Dev()) - 查询所有实体的元数据
- 遍历字段,解析选项集、关联、类型等信息
- 生成 Markdown 和带大纲侧边栏的 HTML 数据字典
基于这个脚本,我们首先开发了 CRM 实体属性查看器------一个 WinForms 桌面应用,左侧实体列表、右侧字段详情,支持搜索、导出等功能。但用户反馈:"我不喜欢在 LINQPad 里直接 Dump 控件"。
于是我们改为独立的 WinForms 窗体,用 Form.ShowDialog() 展示,体验接近 Visual Studio 的解决方案管理器。这个过程中遇到了几个典型问题:
⚠️ 坑 1:WinForms 控件序列化失败
Dump() 对 TableLayoutPanel 等复杂容器控件会抛出 SerializationException。解决方法是:要么单独 Dump 每个控件,要么改用 TabControl 作为容器,或者干脆用独立的 Form。
⚠️ 坑 2:Label 命名冲突
System.Windows.Forms.Label 和 Microsoft.Xrm.Sdk.Label 存在歧义。修复:方法参数使用完全限定名 Microsoft.Xrm.Sdk.Label,WinForms 控件则用 System.Windows.Forms.Label。
⚠️ 坑 3:Invoke 必须在窗口句柄创建后调用
在 Task.Run 的回调中调用 Control.Invoke,如果控件尚未渲染,会抛出异常。解决方案:添加 SafeInvoke 方法检查 IsHandleCreated,或使用 Timer 延迟启动异步操作。
三、第一个 MCP 服务器:CRM 实体查询
学会了 WinForms 控件的各种坑之后,我们决定转向 MCP 方案。MCP 服务器通过 stdio(标准输入/输出)与 AI 客户端通信,使用 JSON-RPC 2.0 协议。这意味着:
- AI 客户端通过 stdin 发送 JSON 请求
- 服务器处理请求,通过 stdout 返回 JSON 响应
- 完全不需要图形界面
第一个 MCP 服务器 CRM_MCP_Server.linq 提供了四个工具:
| 工具名称 | 功能 | 参数 |
|---|---|---|
list_entities |
列出实体,支持搜索过滤 | search(可选) |
get_entity_fields |
获取实体字段详情 | entity_name(必填) |
get_entity_info |
获取实体元数据概要 | entity_name(必填) |
search_entities |
按关键词搜索实体 | keyword(必填) |
在 OpenCode 中配置 MCP 服务器非常简单,只需在 opencode.json 中添加:
// opencode.json
{
"mcp": {
"crm-entity": {
"type": "local",
"command": [
"D:\\...\\lprun.exe",
"-format=text",
"D:\\...\\CRM_MCP_Server.linq"
],
"enabled": true
}
}
}
配置完成后,AI 就可以直接调用这些工具。例如:"查询 Case 相关的实体有多少个"------AI 会调用 search_entities(keyword="case"),返回 34 个匹配实体。
✅ 实战验证
通过 MCP 服务器,AI 成功查询到"非标差价"实体的逻辑名为 new_nonstandardpricedifference,并展示了其 35 个字段的详细信息,包括 Base Ean、Cost Adder、Resale Adder 等业务字段,以及 State/Status 选项集的值。
四、第二个 MCP 服务器:解决方案管理
实体查询成功后,我们扩展了 MCP 的能力,开发了 CRM_Solution_MCP_Server.linq,功能涵盖:
| 工具名称 | 功能 |
|---|---|
list_solutions |
查询解决方案列表(倒序,默认 50 个) |
get_today_solution |
获取当日解决方案(YFT_20260730)是否存在 |
create_solution |
创建当日解决方案 |
add_component |
添加组件(Web 资源/程序集/步骤/实体等) |
remove_component |
从解决方案中移除组件 |
search_components |
搜索可添加的组件 |
migrate_to_uat |
迁移解决方案到 UAT 环境 |
migrate_to_prod |
迁移解决方案到 PROD 环境 |
get_solution_components |
查询解决方案中的组件列表 |
这个服务器的实现复用了现有 LINQPad 脚本中的 Solution 类,包括创建解决方案、导出/导入解决方案、轮询异步作业等逻辑。最复杂的部分是迁移功能,它需要:
- 确保解决方案存在------如果不存在则自动创建
- 导出------从 DEV 环境导出为 .zip 文件
- 导入 ------通过
ExecuteAsync提交异步导入任务 - 轮询------每 5 秒检查一次导入状态
- 发布 ------导入成功后提交
PublishAllXml请求
⚠️ 坑 4:NuGet 包引用与编译问题
在 LINQPad 5 中,通过 lprun -compileonly 编译时,NuGet 包可能不会自动解析。更严重的是,ExportSolutionRequest 等类型在 Microsoft.Crm.Sdk.Messages 命名空间中,但 ExecuteAsyncRequest 却在 Microsoft.Xrm.Sdk.Messages 中。最终我们使用 Microsoft.CrmSdk.XrmTooling.CoreAssembly NuGet 包,并避免使用 $ 字符串插值(LINQPad 5 的编译器对中文 + 插值的兼容性有问题),改用 + 拼接。
五、Skill:把经验固化下来
随着多个脚本和 MCP 服务器的开发,我们创建了 LINQPad 调试 Skill,用于指导 AI 在涉及 LINQPad 5 脚本和 Dynamics 365 CRM 时的行为。Skill 包含了:
- 环境信息------LINQPad 版本、安装路径、扩展脚本位置
- 编译检查命令 ------
lprun -compileonly - 运行时测试命令 ------
lprun -format=text - 可用 UI 控件------WinForms 控件直接 Dump、Util 辅助方法
- 常见编译错误及修复------Label 歧义、序列化失败、丢弃参数不支持等
- 组织服务连接注意事项 ------使用已有的
Dynamic365.GetService_YFT_Dev() - MCP 服务器说明------实体查询和解决方案管理两个 MCP 服务器的配置和使用
Skill 的核心价值在于:让 AI 知道"这个项目有哪些工具、怎么用、有哪些坑"。当 AI 需要编写新的 LINQPad 脚本时,Skill 提供了完整的上下文,避免重复踩坑。
六、架构总结
整个系统的架构如下:
┌─────────────────────────────────────────────────────┐
│ AI 客户端 (OpenCode) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ MCP Client (发起 JSON-RPC 请求) │ │
│ └──────────────┬──────────────────────────────────┘ │
└─────────────────┼────────────────────────────────────┘
│ stdin / stdout (JSON-RPC 2.0)
┌─────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────────┐
│ CRM │ │ CRM │ │ 其他 MCP │
│ 实体查询 │ │ 解决方案 │ │ 服务器... │
│ MCP │ │ MCP │ │ │
│ 服务器 │ │ 服务器 │ │ │
└────┬────┘ └────┬─────┘ └──────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ lprun.exe (LINQPad 命令行运行器) │
│ ┌─────────────────────────────────────┐ │
│ │ Dynamic365.GetService_YFT_Dev() │ │
│ │ → IOrganizationService │ │
│ │ → RetrieveAllEntitiesRequest │ │
│ │ → ExportSolutionRequest │ │
│ │ → AddSolutionComponentRequest │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Dynamics 365 CRM (开发/UAT/生产) │
└─────────────────────────────────────────┘
七、经验与教训
7.1 技术选型
- LINQPad 5 是快速原型开发的利器,但 C# 版本较旧(约 C# 6),不支持
ValueTuple、丢弃参数等新语法 - MCP 协议简单高效,stdio 传输方式适合命令行工具,不需要额外的网络配置
- WinForms 在 LINQPad 中可以作为独立窗体运行,体验接近原生桌面应用
7.2 踩坑清单
| # | 问题 | 解决 |
|---|---|---|
| 1 | Label 命名歧义 |
使用完全限定名 |
| 2 | WinForms 序列化失败 | 改用独立 Form 或分开 Dump |
| 3 | Invoke 窗口句柄未创建 | SafeInvoke + IsHandleCreated 检查 |
| 4 | NuGet 包编译不通过 | 使用 Microsoft.CrmSdk.XrmTooling.CoreAssembly |
| 5 | $ 字符串插值 + 中文兼容问题 |
改用 + 拼接 |
| 6 | MCP 中文乱码 | 设置 Console.InputEncoding 和 OutputEncoding 为 UTF-8 |
7.3 最佳实践
- 不要重复造轮子 ------项目中已有
Dynamic365类和Solution类,直接复用 - 编译检查是底线 ------每次修改后运行
lprun -compileonly,避免运行时才发现语法错误 - 文本模式测试 ------
lprun -format=text可以快速验证运行时是否有异常 - Skill 是团队的记忆------把踩过的坑、环境信息、常用命令都写在 Skill 里,AI 和人类都能受益
八、未来展望
目前我们已经有两个 MCP 服务器在运行,覆盖了 CRM 实体查询和解决方案管理两大场景。接下来可以考虑:
- Web 资源管理------自动发布前端代码到 CRM
- 数据迁移------Excel 导入导出、数据清洗
- 自动化测试 ------调用 CRM 的
WhoAmI等基础 API 验证环境连通性 - 多环境批量操作------一键同步 DEV → UAT → PROD
MCP 生态正在快速发展。从 OpenCode 到 Claude Desktop,越来越多的 AI 客户端支持 MCP 协议。而 LINQPad 作为 .NET 生态中最灵活的脚本工具,与 MCP 的结合为 CRM 开发运维提供了全新的可能性。
🔑 核心感悟
**AI 不是替代开发者,而是放大开发者的能力。**MCP 服务器就像给 AI 装上了一双"手"------它能看到 CRM 的数据、能操作解决方案、能执行迁移。而我们要做的,就是定义好这双手能做哪些事情,以及如何正确地做。Skill 就是这份"使用说明书"。
© 2026 · 基于 LINQPad 5 + Dynamics 365 CRM 的 MCP 实战记录