关于 create-arc1-extension
面向开发者的技能,用于创建本地 TypeScript 扩展,在不分叉 ARC-1 的情况下添加自定义工具。它涵盖 SAP API 与安全策略选择、脚手架、加载及 SAP 实测。
主要功能
- 01清单与 TypeScript 工具实现路径
- 02SAP API 与操作类型选择
- 03写入和执行安全门控
- 04插件脚手架与加载指导
- 05实测与错误解读
适用场景
- 01将现有 SAP 端点封装为 ARC-1 工具
- 02不分叉 ARC-1 添加读写工具
- 03运行获准的控制台类或经典报表
MCP 相关
指导创建向 ARC-1 的 MCP 接口提供自定义工具的扩展,并说明如何在 tools/list 中确认工具已注册。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ arc-mcp/ arc-1 --skill create-arc1-extension安装后在 Claude Code 中输入/create-arc1-extension确认已加载。
详细介绍
创建 ARC-1 扩展
指导开发者构建 ARC-1 扩展——一个本地插件,可在不进行分叉的情况下为 ARC-1 实例添加 Custom_* 工具,并复用 ARC-1 的已认证 SAP 客户端、7 项范围 + allow* 安全上限、审计和 PP。本文总结了构建该框架(PR1–PR5)并在 S/4HANA 上进行实时验证的经验。
实际依据——请先阅读并遵循以下内容:
- 用户指南(请引导开发者查看):
docs_page/extensions.md— 权威操作指南(层级、ctx.http/ctx.run、安全、CF/Docker 部署)。已发布在文档站点的 Using ARC-1 → Extensions (Custom Tools) 下。 - 规范:
docs/research/2026-06-17-extension-framework-spec.md(v1)和extension-framework-v2-spec.md(延期内容)。 - 完整示例:
arc-mcp/arc-1-extension-sample— ADT 和 OData 读取、一个 manifest 工具、Custom_RunClass和Custom_RunReport(受控的命名执行)、一个 OData 写入(Custom_CreateSalesOrder),以及完整的 LISA custom-ICF 集成(Custom_ListLanguages/GetTranslation/SetTranslation)——均已在 S/4HANA 上实时验证(真实 HTTP 201/200 写入)。复制最接近的工具并进行调整。
**v1 的实际情况(务必准确):**读取操作开放(ctx.http.get/head)。写入操作(ctx.http.post/put/delete)仅适用于非 ADT 路径(OData/ICF),且必须启用 SAP_ALLOW_PLUGIN_RAW_WRITES。控制台类和经典报表通过 ctx.run.classRun / ctx.run.programRun 执行(需启用 SAP_ALLOW_PLUGIN_EXECUTE)。ADT 对象写入(通过 /sap/bc/adt/… 写入 CLAS/DDLS/…)始终会被拒绝——这些属于 v2 中支持包感知的 ctx.write。
触发条件
- “为 ARC-1 添加自定义工具 / 插件 / 扩展”
- “将此 SAP/ADT/OData 端点封装为 MCP 工具”
- “不进行分叉,为 ARC-1 构建我自己的工具”
- “基于 ARC-1 构建诊断工具”(SM37/SLG1/gateway 日志等)
步骤 1 — 确定路径(先询问,不要假设)
使用 AskUserQuestion。第一个问题是门槛:
- 后端。 该工具是否通过 HTTP 与 ARC-1 连接的同一个 SAP 系统通信(ADT、
OData 或自定义 ICF/REST 服务)?
- 否 — 不同的 SAP 产品(Cloud ALM、BTP 服务、BW、HANA、Datasphere、SuccessFactors) 或非 HTTP 协议(原生 RFC、SAP GUI 脚本)→ 这不是扩展。 它是一个独立的 MCP 服务 (基于 BTP 身份验证模块构建,走“自有服务”路径)。到此为止,并引导他们前往对应路径。
- 是 → 继续。
- 层级。
- 清单层(声明式 JSON,无代码)——如果工具执行的是“验证输入 → 一次读取 GET → 返回”。 没有逻辑,也不写入数据。
- 代码层(
defineTool,TypeScript)——如果需要逻辑、响应整形、多个读取操作、向 OData/ICF 服务 写入(ctx.http.post/put/delete),或执行控制台类或经典报表 (ctx.run.classRun/ctx.run.programRun)。
- SAP API — ADT(
/sap/bc/adt/…)、OData(/sap/opu/odata/…)或自定义 ICF (/sap/bc/http/…)。 对于自定义端点:它必须已存在于 SAP 上——扩展不包含任何 ABAP 代码。 - 功能以及范围和opType:
- 读取(上述任意一种 API)→
scope: 'read'、opType: OperationType.Read— 使用ctx.http.get。 - 写入 OData/ICF 服务(
ctx.http.post/put/delete)→scope: 'write'、opTypeCreate/Update/Delete。除非管理员设置SAP_ALLOW_PLUGIN_RAW_WRITES=true+SAP_ALLOW_WRITES=true,否则会被拒绝。路径必须是非 ADT 路径 (/sap/opu/odata/…或/sap/bc/http/…)。 - 执行控制台类(
IF_OO_ADT_CLASSRUN)→scope: 'write'、opType: OperationType.Workflow— 使用ctx.run.classRun。除非设置SAP_ALLOW_PLUGIN_EXECUTE=true+SAP_ALLOW_WRITES=true,否则会被拒绝。 - 执行经典报表(
PROG,本地部署)→scope: 'write'、opType: OperationType.Workflow、availableOn: 'onprem'— 使用ctx.run.programRun。输入仅为名称, 输出仅为文本:不支持选择参数或变式。使用相同的执行和写入门槛。 - ADT 对象创建/更新/删除(通过
/sap/bc/adt/…操作 CLAS/DDLS/…)→ v1 中不可用 — 始终会被拒绝;这是 v2 中支持包的ctx.write。如果工具需要此功能,请说明并停止。
- 读取(上述任意一种 API)→
步骤 2 — 搭建脚手架(参照 arc-1-extension-sample)
创建一个新的仓库 arc1-plugin-<name>(纯 TS,无 ABAP):
package.json—"type":"module"、peerDep"arc-1": ">=<ver>"、devDepstypescript+zod, 构建命令为"tsc && node -e \"require('node:fs').cpSync('manifests','dist/manifests',{recursive:true})\""(仅在包含清单时)。可选的"arc1": { "apiVersion": 1 }块是前向声明——v1 中,加载器从Plugin默认导出(src/index.ts)读取apiVersion;而requires:{scopes,packages}属于 v2 (已声明但尚未强制执行),因此不要依赖它。- 读取(代码层) →
src/tools/Custom_<X>.ts:import { z } from 'zod'; import { defineTool, OperationType } from 'arc-1/public'; export default defineTool({ name: 'Custom_<X>', // MUST start with Custom_ description: '…', schema: z.object({ /* … */ }), policy: { scope: 'read', opType: OperationType.Read }, async handler(args, ctx) { const res = await ctx.http.get(`/sap/bc/adt/…`, { Accept: 'text/plain' }); return { content: [{ type: 'text', text: /* shape res.body */ }] }; }, }); - 清单层 →
manifests/Custom_<X>.tool.json:{ "name": "Custom_<X>", "description": "…", "scope": "read", "inputSchema": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": { "type": "string", "pattern": "^[A-Za-z0-9_/]{1,40}$" } } }, "request": { "method": "GET", "path": "/sap/bc/adt/…/{name}/source/main", "pathParams": { "name": "$.name" }, "accept": "text/plain" }, "response": { "maxBytes": 50000 } } - 写入(代码层) — OData / 自定义 ICF 的
POST/PUT/DELETE→src/tools/Custom_<X>.ts:import { z } from 'zod'; import { defineTool, OperationType } from 'arc-1/public'; export default defineTool({ name: 'Custom_<X>', description: 'Create something via an OData/ICF service.', schema: z.object({ /* … */ }), policy: { scope: 'write', opType: OperationType.Create }, // POST→Create / PUT→Update / DELETE→Delete async handler(args, ctx) { const body = JSON.stringify(/* entity / payload */); // path MUST be non-ADT (OData/ICF); CSRF is fetched + attached automatically. const res = await ctx.http.post('/sap/opu/odata/<ns>/<SERVICE>/<EntitySet>', body, 'application/json', { Accept: 'application/json' }); return { content: [{ type: 'text', text: `HTTP ${res.statusCode}\n${res.body}` }] }; }, }); - 执行(代码层) — 运行控制台类或经典报表 →
src/tools/Custom_<X>.ts。 控制台类示例:
经典报表示例:import { z } from 'zod'; import { defineTool, OperationType } from 'arc-1/public'; export default defineTool({ name: 'Custom_<X>', description: 'Execute an ABAP console class and return its output.', schema: z.object({ className: z.string().min(1).max(40) }), policy: { scope: 'write', opType: OperationType.Workflow }, // execute ⇒ write-class op async handler(args, ctx) { const out = await ctx.run.classRun((args as { className: string }).className); // gated; see Step 1.4 return { content: [{ type: 'text', text: out }] }; }, });import { z } from 'zod'; import { defineTool, OperationType } from 'arc-1/public'; export default defineTool({ name: 'Custom_<X>', description: 'Execute a classic ABAP report and return SAP list or error text.', schema: z.object({ reportName: z.string().min(1).max(40) }), policy: { scope: 'write', opType: OperationType.Workflow }, availableOn: 'onprem', async handler(args, ctx) { const out = await ctx.run.programRun((args as { reportName: string }).reportName); return { content: [{ type: 'text', text: out }] }; }, });programRun只接受报表名称。如果报表需要运行时输入,请使用一个小型的IF_OO_ADT_CLASSRUN类。 两种命名操作都会原样返回 SAP 响应文本;SAP 可能会在 HTTP 200 响应中返回对象缺失或执行错误。 src/index.ts—export default { name, version, apiVersion: 1, tools: [...], manifests: ['manifests/Custom_<X>.tool.json'] } satisfies Plugin;- README — 说明功能 + 加载命令。
步骤 3 — 构建 + 加载 + 测试(已在线验证)
# until arc-1 is published with the public API, link the local build:
( cd /path/to/arc-1 && npm link )
npm install && npm link arc-1 && npm run build
# load into an instance…
ARC1_PLUGINS=$PWD/dist/index.js arc1 --transport http-streamable
# …or drive one read call (args MUST be --json, not positional):
ARC1_PLUGINS=$PWD/dist/index.js arc1-cli call Custom_<X> --json '{"name":"RSPARAM"}'
# …a WRITE tool (OData/ICF) needs the raw-write opt-ins (else it's refused):
SAP_ALLOW_PLUGIN_RAW_WRITES=true SAP_ALLOW_WRITES=true \
ARC1_PLUGINS=$PWD/dist/index.js arc1-cli call Custom_<X> --json '{ … }'
# …an EXECUTE tool needs the execute opt-ins:
SAP_ALLOW_PLUGIN_EXECUTE=true SAP_ALLOW_WRITES=true \
ARC1_PLUGINS=$PWD/dist/index.js arc1-cli call Custom_<X> --json '{"className":"ZCL_FOO"}'
# …or, for an on-premise classic report:
SAP_ALLOW_PLUGIN_EXECUTE=true SAP_ALLOW_WRITES=true \
ARC1_PLUGINS=$PWD/dist/index.js arc1-cli call Custom_<X> --json '{"reportName":"ZREPORT"}'
请查看结果,而不只是退出状态。若被安全门拒绝,则会报 AdtSafetyError(“…disabled”/“may not write to an ADT path”);若是 SAP 端的问题(路径错误、缺少服务、载荷无效),则会报 AdtApiError,其中包含 SAP 状态码和响应正文——这意味着安全门已放行,且请求已到达 SAP(这是有用的信号)。具名类/报表的执行是个例外:SAP 可能会在 HTTP 200 文本响应中返回执行错误,此时 ctx.run 会原样返回该响应。根据 SAP 错误信息调整路径/载荷;不要仅凭 HTTP 状态码推断成功。
确认该工具出现在 tools/list 中,且调用返回真实的 SAP 数据。若要将插件部署到 BTP Cloud Foundry 或 Docker(所有者检查/--chown 易踩的坑,以及镜像、buildpack、卷之间的权衡),请指引开发者参阅 docs_page/extensions.md 中的 部署扩展 部分。
常见陷阱(都是吃过亏才总结出来的)
- 必须使用
Custom_命名空间,发生冲突时会导致服务启动失败(快速失败)。 ctx.http可自由读取(GET/HEAD);写入(post/put/delete)仅适用于非 ADT 路径,且只有管理员设置SAP_ALLOW_PLUGIN_RAW_WRITES=true(以及SAP_ALLOW_WRITES=true),并且工具声明了scope:'write'时才可执行。对/sap/bc/adt/…对象路径的写入始终会被拒绝(无法在原始写入时强制执行软件包允许列表——ADT 对象写入属于 v2ctx.write词汇表)。系统会自动获取并附加 CSRF 令牌。此功能适用于 OData / 自定义 ICF 写入服务。ctx.client是运行时的纯读取视图——运行时会屏蔽.http/.safety以及数据/SQL 读取(getTableContents/runQuery/runTableQuery)(强制类型转换会得到undefined)。v1 插件没有数据/SQL 接口;请使用纯读取方法(或ctx.http.get)。policy.opType必须与scope匹配——声明的 scope 必须涵盖 opType 所需的 scope(例如,opType:'U'需要scope:'write'),否则插件会导致服务启动失败。请确保它们与上述示例保持一致。- 命名执行属于特权操作。
ctx.run.classRun(name)会运行一个IF_OO_ADT_CLASSRUN控制台类;ctx.run.programRun(name)会运行本地部署的传统报表,并返回其列表/错误文本(不支持选择条件或变式)。两者都需要SAP_ALLOW_PLUGIN_EXECUTE=true以及SAP_ALLOW_WRITES=true以及一个write作用域的工具;名称会经过验证。默认关闭。 - **OData 路径发现——403
/IWFND/MED/170 "No service found"通常表示路径错误,**不只是服务未激活。服务名称和命名空间都很重要:例如,EPM 演示服务的路径是/sap/opu/odata/iwbep/GWSAMPLE_BASIC,而不是/sap/opu/odata/sap/ZGWSAMPLE_BASIC。通过GET …/$metadata查找实际路径(200 = 正确;403 = 路径错误或确实未激活 →/IWFND/MAINT_SERVICE)。 - OData V2 创建注意事项:
POST不得携带$format=json查询选项(它属于 SystemQueryOption →400 "not allowed for this Request Type")。请改用Accept标头协商 JSON。必填实体字段各不相同——使用GET …/<EntitySet>?$top=1查看结构。 - 自定义 ICF(LISA 风格)服务:通常使用
POST /sap/bc/http/sap/<SERVICE>/<action>,并附带JSON 正文(操作在 URL 路径中;正文中是参数)。由此产生两个影响:(1) 如果服务也使用POST进行读取,这些读取工具仍需要SAP_ALLOW_PLUGIN_RAW_WRITES+scope:'write'(ctx.http按 HTTP 方法进行门控)——声明opType: Read,以确保操作类型如实;(2) 写入可能需要未关闭的传输请求——使用SAPTransport创建一个(在没有 STMS 路由的系统上,它是本地请求,这没问题)。 - **写入请求到达 SAP ≠ 获得 2xx。**门控、CSRF 和 POST 都可能成功,但 SAP 仍会返回 4xx(负载错误、服务未激活、缺少传输请求)。这属于
AdtApiError,而非门控拒绝——调整请求即可,框架已尽到职责。 - 清单层级 = 只读 GET,必须设置
additionalProperties:false;path是不含主机的模板,路径参数须进行百分比编码(确保无法路径遍历)。v1 不支持 POST/正文。 availableOn: 'onprem' | 'btp'(可选,默认为all)会在解析出的系统类型不匹配时,将工具从tools/list中隐藏。超聚焦模式下不会显示任何插件工具。ctx上的elicit/notify/sampling受能力门控——仅当 MCP 客户端支持这些功能时才会提供(CLI/stdio 路径中不存在)。- 使用
arc-1/public/testing中的createMockToolContext对处理程序进行单元测试(记录ctx.http/ctx.run.classRun/ctx.run.programRun调用,返回已配置的输出——无需连接实时 SAP)。 - 管理员终止开关:
SAP_DENY_ACTIONS=Custom_*(全部)或Custom_Foo(单个)会移除插件工具。
部署(有人询问时)
请参阅 docs_page/extensions.md 中的部署扩展。
关键信息:插件是从 ARC1_PLUGINS 绝对路径加载的本地文件(不会展开 $HOME);在 BTP CF 上,请使用派生的 Docker 镜像(FROM ghcr.io/arc-mcp/arc-1、COPY --chown=arc1:arc1
— 普通的 COPY 会将文件设为 root 所有,而加载器会拒绝非所有者所有或对所有用户可写的文件)或
将构建出的 dist/ 一同部署到 buildpack 应用文件中(/home/vcap/app/...,归 vcap 所有)。不支持热重载(更改后需重新部署)。添加插件无需更改 XSUAA(会复用现有作用域)。