作者:liuxy | 适用版本:SAP S/4HANA 2025 / ECC 6.0 EHP8 | 阅读时长:约 15 分钟
一、为什么要把 MCP Server 做进 SAP 内部
Model Context Protocol(MCP,模型上下文协议)这两年火得一塌糊涂。它的思路很简单:让 LLM 通过统一的 JSON-RPC 2.0 接口去调用外部工具——查数据库、调 API、读文件、跑报表。
社区里很快冒出了大量 ABAP MCP Server 实现:mcp-abap-abap-adt-api、abap-ai/mcp、AWS 的 ABAP Accelerator……但仔细看,它们几乎都有一个共同点——MCP Server 跑在 SAP 系统之外(Python / Node / Docker),通过 RFC / ADT REST / OData 反向连接 SAP。
跑着跑着,问题就来了:
| 问题 | 外部 MCP Server | SAP 内部 MCP Server |
|---|---|---|
| 工具数量爆炸 | 每新增一个业务场景都要在 Python 端加一个 tool | ICF Handler + 配置表动态注册 |
| 权限链路 | 一般用技术 RFC 用户,绕过 SAPGUI 的 AUTHORITY-CHECK | 直接以当前会话用户身份执行,调用方权限即真实权限 |
| 审计追溯 | 散落在外部服务的日志里,跨系统对账 | 写一张透明表,审计同事直接 SE16 查 |
| 网络拓扑 | SAP 网关需对办公网开端口 | ICF 节点全部位于 SAP 内网 |
| 部署复杂度 | 多一套运行时(Python venv / Docker / K8s) | 一段 Z 类 + 一个 SICF 服务,搞定 |
尤其是权限这一条——basis lead 看完架构图问一句"这个服务跑在哪个用户下",基本上就过不了安全评审。
所以我们在去年立项的时候做了一个大胆的决定:把 MCP Server 完整地跑进 SAP 内部,让 SAP 系统本身成为 MCP Server。本文就是把这条路线从架构到代码完整讲一遍。
二、整体架构
整个 SAP 侧只对外暴露 一个 ICF 节点。所有 MCP 交互——initialize、tools/list、tools/call——都打这一个 URL。MCP Server 内部按 JSON-RPC 的 method 字段做路由。
┌────────────┐ HTTP/JSON-RPC ┌─────────────────────────────────┐
│ MCP Client│ ───────────────►│ SICF Node (单端点) │
│ (Cursor / │ │ └─ ZCL_MCP_HTTP_HANDLER │
│ Claude / │ │ └─ ZCL_MCP_JSONRPC │
│ 自研Agent)│ │ └─ ZCL_MCP_SERVER │
└────────────┘ │ └─ ZCL_MCP_REGISTRY│
│ └─ ZIF_MCP_TOOL│
│ (各 Z*_TOOL)│
└─────────────────────────────────┘
│
▼
SAP 业务表 / 函数 / 类
五大核心类职责清晰:
- ZCL_MCP_HTTP_HANDLER — ICF Handler,所有 HTTP 入口都从这走
- ZCL_MCP_JSONRPC — JSON-RPC 2.0 协议解析层,负责 method 路由、id 匹配、错误码
- ZCL_MCP_SERVER — MCP 协议层,
initialize握手、tools/list、tools/call三件事 - ZCL_MCP_REGISTRY — 工具注册中心,配置表
ZMCP_TOOLS存"工具名 → 类名 + 启用标志" - ZIF_MCP_TOOL — 工具接口,每个 tool 实现一次
三、核心代码实现
3.1 ICF Handler(最薄的一层)
ICF Handler 只做四件事:读 body、解析 method、调用 JSON-RPC 处理器、把结果写回去。这一层不放任何业务逻辑。
CLASS zcl_mcp_http_handler DEFINITION
PUBLIC
INHERITING FROM cl_http_ext_server
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS if_http_extension~handle_request
REDEFINITION.
PRIVATE SECTION.
DATA: go_jsonrpc TYPE REF TO zcl_mcp_jsonrpc.
ENDCLASS.
CLASS zcl_mcp_http_handler IMPLEMENTATION.
METHOD if_http_extension~handle_request.
DATA: lv_body TYPE string,
lv_response TYPE string.
" 1) 读请求体
lv_body = server->request->get_cdata( ).
" 2) 委托给 JSON-RPC 处理层
CREATE OBJECT go_jsonrpc.
lv_response = go_jsonrpc->dispatch( iv_request = lv_body
io_server = server ).
" 3) 写响应
server->response->set_cdata( lv_response ).
server->response->set_header_field( name = 'Content-Type'
value = 'application/json' ).
ENDMETHOD.
ENDCLASS.
在 SICF 里挂到这个 Handler 类即可。路径示例:
/sap/bc/zcmcp。
3.2 JSON-RPC 2.0 解析层
MCP 协议基于 JSON-RPC 2.0。必须正确处理 id、method、params、error.code。下面是最小可用版本:
CLASS zcl_mcp_jsonrpc DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS dispatch
IMPORTING iv_request TYPE string
io_server TYPE REF TO if_http_server
RETURNING VALUE(rv_response) TYPE string.
PRIVATE SECTION.
METHODS parse_request
IMPORTING iv_json TYPE string
EXPORTING ev_method TYPE string
ev_id TYPE string
ed_params TYPE data
ev_error TYPE string.
METHODS build_response
IMPORTING iv_id TYPE string
iv_result TYPE string
RETURNING VALUE(rv_response) TYPE string.
METHODS build_error
IMPORTING iv_id TYPE string
iv_code TYPE i
iv_msg TYPE string
RETURNING VALUE(rv_response) TYPE string.
ENDCLASS.
CLASS zcl_mcp_jsonrpc IMPLEMENTATION.
METHOD dispatch.
DATA: lv_method TYPE string,
lv_id TYPE string,
lv_params TYPE string,
lv_err TYPE string,
lv_result TYPE string.
parse_request( EXPORTING iv_json = iv_request
IMPORTING ev_method = lv_method
ev_id = lv_id
ed_params = lv_params
ev_error = lv_err ).
IF lv_err IS NOT INITIAL.
rv_response = build_error( iv_id = lv_id
iv_code = -32700
iv_msg = lv_err ).
RETURN.
ENDIF.
" 根据 method 路由
CASE lv_method.
WHEN 'initialize'.
lv_result = zcl_mcp_server=>handle_initialize( lv_params ).
WHEN 'tools/list'.
lv_result = zcl_mcp_server=>handle_tools_list( ).
WHEN 'tools/call'.
lv_result = zcl_mcp_server=>handle_tools_call( lv_params ).
WHEN OTHERS.
rv_response = build_error( iv_id = lv_id
iv_code = -32601
iv_msg = 'Method not found' ).
RETURN.
ENDCASE.
rv_response = build_response( iv_id = lv_id
iv_result = lv_result ).
ENDMETHOD.
METHOD build_response.
rv_response = |\{\"jsonrpc\":\"2.0\",\"id\":\"{ iv_id }\",\"result\":{ iv_result }\}|.
ENDMETHOD.
METHOD build_error.
rv_response = |\{\"jsonrpc\":\"2.0\",\"id\":\"{ iv_id }\",\"error\":\{\"code\":{ iv_code },\"message\":\"{ iv_msg }\"\}\}|.
ENDMETHOD.
ENDCLASS.
3.3 MCP Server(业务调度核心)
tools/list 时去注册表里把启用的工具全部捞出来,组装成 LLM 能理解的 schema;tools/call 时按名字动态创建实例并执行。
CLASS zcl_mcp_server DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
CLASS-METHODS handle_initialize
IMPORTING iv_params TYPE string
RETURNING VALUE(rv_result) TYPE string.
CLASS-METHODS handle_tools_list
RETURNING VALUE(rv_result) TYPE string.
CLASS-METHODS handle_tools_call
IMPORTING iv_params TYPE string
RETURNING VALUE(rv_result) TYPE string.
ENDCLASS.
CLASS zcl_mcp_server IMPLEMENTATION.
METHOD handle_initialize.
" MCP 规范要求返回协议版本和能力声明
rv_result = |\{\"protocolVersion\":\"2025-06-18\"," &&
|\"capabilities\":\{\"tools\":\{\}\}," &&
|\"serverInfo\":\{\"name\":\"abap-mcp\",\"version\":\"1.0.0\"\}\}|.
ENDMETHOD.
METHOD handle_tools_list.
DATA: lt_tools TYPE TABLE OF zcmcp_tool_def,
ls_tool LIKE LINE OF lt_tools,
lv_json TYPE string.
" 从注册表读所有启用工具
SELECT * FROM zmcp_tools
INTO TABLE lt_tools
WHERE active = abap_true.
LOOP AT lt_tools INTO ls_tool.
DATA(lo_tool) = CAST zif_mcp_tool(
NEW (ls_tool-class_name) ).
lv_json = lv_json && ',' &&
lo_tool->get_definition( ).
ENDLOOP.
rv_result = |\{\"tools\":[ \{ lv_json+1 \} ]\}|.
ENDMETHOD.
METHOD handle_tools_call.
DATA: lv_tool_name TYPE string,
lv_args_json TYPE string,
lo_tool TYPE REF TO zif_mcp_tool.
" 极简解析:从 params 里抠出 name 和 arguments
lv_tool_name = zcl_mcp_util=>extract_json_str( iv_json = iv_params iv_key = 'name' ).
lv_args_json = zcl_mcp_util=>extract_json_obj( iv_json = iv_params iv_key = 'arguments' ).
" 查注册表 → 动态实例化
SELECT SINGLE class_name FROM zmcp_tools
WHERE tool_name = lv_tool_name AND active = @abap_true
INTO @DATA(lv_class).
CHECK sy-subrc = 0.
CREATE OBJECT lo_tool TYPE (lv_class).
" 调用工具
DATA(lv_result) = lo_tool->execute( iv_args_json ).
rv_result = |\{\"content\":[\{\"type\":\"text\",\"text\":\"\{ lv_result \}\"\}\]," &&
|\"isError\":false\}|.
ENDMETHOD.
ENDCLASS.
3.4 工具接口与示例实现
每个工具实现这个接口,框架就能把它登记到 MCP 客户端。
INTERFACE zif_mcp_tool PUBLIC.
METHODS get_definition
RETURNING VALUE(rv_json) TYPE string.
METHODS execute
IMPORTING iv_args_json TYPE string
RETURNING VALUE(rv_result) TYPE string.
ENDINTERFACE.
来一个真实场景的工具示例:按销售订单号查客户名称。
CLASS zcl_mcp_tool_get_customer DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_mcp_tool.
ENDCLASS.
CLASS zcl_mcp_tool_get_customer IMPLEMENTATION.
METHOD zif_mcp_tool~get_definition.
rv_json =
|\{"name":"get_customer_by_so","description":| &&
|"根据销售订单号查询对应客户名称",| &&
|"inputSchema":\{| &&
|"type":"object",| &&
|"properties":\{"vbeln":\{"type":"string","description":"销售订单号"\}\},| &&
|"required":["vbeln"]\}\}|.
ENDMETHOD.
METHOD zif_mcp_tool~execute.
DATA: lv_vbeln TYPE vbeln,
lv_kunnr TYPE kunnr,
lv_name1 TYPE name1.
lv_vbeln = zcl_mcp_util=>extract_json_str( iv_json = iv_args_json iv_key = 'vbeln' ).
SELECT SINGLE k~kunnr, k~name1
FROM vbak AS v
INNER JOIN kna1 AS k ON k~kunnr = v~kunnr
WHERE v~vbeln = @lv_vbeln
INTO (@lv_kunnr, @lv_name1).
IF sy-subrc = 0.
rv_result = |客户编号:{ lv_kunnr },客户名称:{ lv_name1 }|.
ELSE.
rv_result = |未找到销售订单 { lv_vbeln } 对应的客户|.
ENDIF.
ENDMETHOD.
ENDCLASS.
然后在 ZMCP_TOOLS 表里插一条:
| TOOL_NAME | CLASS_NAME | ACTIVE |
|---|---|---|
| GET_CUSTOMER_BY_SO | ZCL_MCP_TOOL_GET_CUSTOMER | X |
工具就上线了。客户端刷新一下 tools/list,LLM 自动知道什么时候该调它。
四、踩坑实录(这部分价值千金)
4.1 客户端报"connect timeout",服务压根没收到请求
症状:本地 Python MCP Client 启动后报 requests.exceptions.ConnectTimeout,但 SAP 系统用 SMICM 看 HTTP 端口监听正常。
根因:ICF 节点默认需要登录态。没登录的用户访问 /sap/bc/zcmcp 会被拦在前面的 ICM / Web Dispatcher 上。如果是直接连 SAP(不走 Web Dispatcher),要确认 icm/HTTP/admin_0 里的端口对外开放;如果是经过 Web Dispatcher,需要把 /sap/bc/zcmcp 加到白名单或单独建虚拟主机。
我们的方案:
- 内部测试环境:直接走 SAP HTTP 端口,
SMICM→Administration→ 确认端口 LISTENING - 生产环境:Web Dispatcher 上为 MCP 单独配一个虚拟主机,限定只放行业务网段
- 客户端必须带 Basic Auth:
-u USER:PASS
import requests
r = requests.post(
"http://10.125.1.143:8010/sap/bc/zcmcp",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {}}
},
auth=("BASIS_USER", "secret"),
timeout=30
)
4.2 JSON-RPC 响应里中文乱码
症状:工具正常返回,但中文变成 ??????? 或者 \u????。
根因:ICF Handler 默认字符集是 utf-8,但 ABAP 字符串里 NLV 文本可能在某些场景走 4103(EUC-CN)编码。
修复:
server->response->set_header_field( name = 'Content-Type'
value = 'application/json; charset=utf-8' ).
" 构造响应前对 JSON 字符串做一次强转
lv_response = zcl_mcp_util=>ensure_utf8( lv_response ).
4.3 工具动态实例化时 CREATE OBJECT ... TYPE (lv_class) 报错
症状:开发机运行没问题,到了生产机报 CX_SY_CREATE_OBJECT_ERROR。
根因:生产机 Z 包的 transport 没有同步,或者类没有激活。
修复:
- 每次发版后让 basis 在生产执行
SE24 → ZCL_MCP_TOOL_GET_CUSTOMER → 检查 → 激活 - 更稳妥的做法是用接口引用 +
TRY ... CATCH cx_sy_create_object_error:
TRY.
CREATE OBJECT lo_tool TYPE (lv_class).
CATCH cx_sy_create_object_error.
" 工具实例化失败 → 返回结构化错误
rv_result = |\{\"isError\":true,"content":[{"type":"text","text":"工具未注册或未激活:\{ lv_class \}"\}]\}|.
RETURN.
ENDTRY.
4.4 AUTHORITY-CHECK 永远通过(最危险的坑)
我们 security 团队在自审代码的时候发现:派发层对所有端点都做 AUTHORITY-CHECK S_DEVELOP ID 'ACTVT' FIELD '03',全部用 display。这意味着任何登录用户都能"查看"敏感的财务 / HR 表数据。
修复方案:工具自己声明所需权限对象,调用前由 ZCL_MCP_SERVER 强制校验:
" 在 ZCL_MCP_TOOL_GET_CUSTOMER 的 execute 里第一行加:
AUTHORITY-CHECK OBJECT 'V_VBAK_VKO'
ID 'VKORG' FIELD '1000'
ID 'VTWEG' FIELD '10'
ID 'SPART' FIELD '00'
ID 'ACTVT' FIELD '03'.
IF sy-subrc <> 0.
rv_result = '权限不足:缺少销售订单显示权限'.
RETURN.
ENDIF.
更优雅:把所需权限对象做成工具的元数据(AUTHORITY_OBJECT 字段),由派发层在工具执行前自动做一次集中校验,避免每个工具漏写。
4.5 工具太多导致 prompt 超长
症状:上线 30 个工具后,客户端 tools/list 返回 200KB+ JSON,LLM 直接吐 “context length exceeded”。
修复:
tools/list不返回完整 schema,只返回name + 一句话 description- 真正要调用某个工具时,再调
tools/schema?name=xxx拿完整 schema - 这需要改 MCP Server 的实现,做"懒加载 schema"。我们做了,效果立竿见影,token 占用从 200KB 降到 6KB。
五、与其他 ABAP MCP 路线的对比
| 方案 | 位置 | 权限链路 | 适合场景 |
|---|---|---|---|
| mcp-abap-abap-adt-api | 外部(Node) | ADT 用户(一般是 DEVELOPER 类高权限用户) | 个人开发、AI 辅助编程 |
| abap-ai/mcp SDK | SAP 内部(ICF) | 当前会话用户 | 业务系统对接、多用户 |
| AWS ABAP Accelerator | 外部(Docker) | 可配 Principal Propagation | AWS 生态、多 SAP 系统 |
| 本文方案 | SAP 内部(ICF) | 当前会话用户 + 注册表动态管控 | 自研、ECC 6.0 等无 BTP 环境的场景 |
如果你的目标是 让业务用户用自然语言跑 SAP 报表——选 SAP 内部方案,权限链路天然顺;
如果你的目标是 让 AI 帮开发者写 ABAP 代码——外部 ADT MCP 路线更轻量。
六、写在最后
把 MCP Server 做进 SAP 内部,听起来很激进,做完你会发现它是最朴素的方案:
- 不引入新运行时
- 不绕开 SAP 的权限模型
- 审计、监控、传输都跟着 SAP 走
- 工具的扩缩只是改一张表
最大的代价是:所有扩展点都在 ABAP 这侧,前端 / AI 工程师必须懂点 ABAP。但反过来,业务顾问只要会写 SELECT,就能封装一个工具——这把 AI 落地到核心业务系统的门槛反而降了。
下一步我们准备做的:
- 工具版本管理:
ZMCP_TOOLS加VERSION字段,LLM 调错版本时拒绝 - Streamable HTTP 支持:把 SSE 流式响应也做出来
- 审计表:
ZMCP_AUDIT记录USERNAME / TIMESTAMP / TOOL_NAME / PARAMS / RESULT_LEN
代码已经放在我们内部 GitLab 上。如果读者朋友有类似场景或者想接入 SAP 系统的 MCP Server,欢迎评论区交流。下篇文章会写如何把 SAP 标准 BAPI 自动注册成 MCP 工具,让整个 ERP 一夜之间对 AI “可调用”,敬请期待。
参考资料:
- Model Context Protocol 官方规范(2025-06-18):https://modelcontextprotocol.io
- abap-ai/mcp SDK:https://github.com/abap-ai/mcp
- SAP Community: Building a Native MCP Server in SAP ABAP

1030

被折叠的 条评论
为什么被折叠?



