在 SAP S/4HANA 内部基于 ICF Handler 从零实现 ABAP MCP Server:架构、代码与踩坑实录

作者: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 ServerSAP 内部 MCP Server
工具数量爆炸每新增一个业务场景都要在 Python 端加一个 toolICF 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 交互——initializetools/listtools/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/listtools/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。必须正确处理 idmethodparamserror.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_NAMECLASS_NAMEACTIVE
GET_CUSTOMER_BY_SOZCL_MCP_TOOL_GET_CUSTOMERX

工具就上线了。客户端刷新一下 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 端口,SMICMAdministration → 确认端口 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”。

修复

  1. tools/list 不返回完整 schema,只返回 name + 一句话 description
  2. 真正要调用某个工具时,再调 tools/schema?name=xxx 拿完整 schema
  3. 这需要改 MCP Server 的实现,做"懒加载 schema"。我们做了,效果立竿见影,token 占用从 200KB 降到 6KB。

五、与其他 ABAP MCP 路线的对比

方案位置权限链路适合场景
mcp-abap-abap-adt-api外部(Node)ADT 用户(一般是 DEVELOPER 类高权限用户)个人开发、AI 辅助编程
abap-ai/mcp SDKSAP 内部(ICF)当前会话用户业务系统对接、多用户
AWS ABAP Accelerator外部(Docker)可配 Principal PropagationAWS 生态、多 SAP 系统
本文方案SAP 内部(ICF)当前会话用户 + 注册表动态管控自研、ECC 6.0 等无 BTP 环境的场景

如果你的目标是 让业务用户用自然语言跑 SAP 报表——选 SAP 内部方案,权限链路天然顺;
如果你的目标是 让 AI 帮开发者写 ABAP 代码——外部 ADT MCP 路线更轻量。

六、写在最后

把 MCP Server 做进 SAP 内部,听起来很激进,做完你会发现它是最朴素的方案:

  • 不引入新运行时
  • 不绕开 SAP 的权限模型
  • 审计、监控、传输都跟着 SAP 走
  • 工具的扩缩只是改一张表

最大的代价是:所有扩展点都在 ABAP 这侧,前端 / AI 工程师必须懂点 ABAP。但反过来,业务顾问只要会写 SELECT,就能封装一个工具——这把 AI 落地到核心业务系统的门槛反而降了。

下一步我们准备做的:

  1. 工具版本管理ZMCP_TOOLSVERSION 字段,LLM 调错版本时拒绝
  2. Streamable HTTP 支持:把 SSE 流式响应也做出来
  3. 审计表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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Andrew.Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值