mcpField
C
create-arc1-extension

create-arc1-extension

github.com/arc-mcp开发239219 次安装2 天前更新
代码仓库

关于 create-arc1-extension

面向开发者的技能,用于创建本地 TypeScript 扩展,在不分叉 ARC-1 的情况下添加自定义工具。它涵盖 SAP API 与安全策略选择、脚手架、加载及 SAP 实测。

主要功能

  1. 01清单与 TypeScript 工具实现路径
  2. 02SAP API 与操作类型选择
  3. 03写入和执行安全门控
  4. 04插件脚手架与加载指导
  5. 05实测与错误解读

适用场景

  1. 01将现有 SAP 端点封装为 ARC-1 工具
  2. 02不分叉 ARC-1 添加读写工具
  3. 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。第一个问题是门槛:

  1. 后端。 该工具是否通过 HTTP 与 ARC-1 连接的同一个 SAP 系统通信(ADT、 OData 或自定义 ICF/REST 服务)?
    • 否 — 不同的 SAP 产品(Cloud ALM、BTP 服务、BW、HANA、Datasphere、SuccessFactors) 或非 HTTP 协议(原生 RFC、SAP GUI 脚本)→ 这不是扩展。 它是一个独立的 MCP 服务 (基于 BTP 身份验证模块构建,走“自有服务”路径)。到此为止,并引导他们前往对应路径。
    • 是 → 继续。
  2. 层级。
    • 清单层(声明式 JSON,无代码)——如果工具执行的是“验证输入 → 一次读取 GET → 返回”。 没有逻辑,也不写入数据。
    • 代码层(defineTool,TypeScript)——如果需要逻辑、响应整形、多个读取操作、向 OData/ICF 服务 写入(ctx.http.post/put/delete),或执行控制台类或经典报表 (ctx.run.classRun / ctx.run.programRun)。
  3. SAP API — ADT(/sap/bc/adt/…)、OData(/sap/opu/odata/…)或自定义 ICF (/sap/bc/http/…)。 对于自定义端点:它必须已存在于 SAP 上——扩展不包含任何 ABAP 代码。
  4. 功能以及范围和opType:
    • 读取(上述任意一种 API)→ scope: 'read'、opType: OperationType.Read — 使用 ctx.http.get。
    • 写入 OData/ICF 服务(ctx.http.post/put/delete)→ scope: 'write'、opType Create/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。如果工具需要此功能,请说明并停止。

步骤 2 — 搭建脚手架(参照 arc-1-extension-sample)

创建一个新的仓库 arc1-plugin-<name>(纯 TS,无 ABAP):

  • package.json — "type":"module"、peerDep "arc-1": ">=<ver>"、devDeps typescript+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 对象写入属于 v2 ctx.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(会复用现有作用域)。

评论

支持 Markdown