Skill: zwwz22/xiaobai-print (version 1.0.0)

--- name: xiaobai-print description: "小白打印助手。通过仓库内置 wrapper 调用本地 MCP bridge，执行设备检查、能力确认、文件上传、任务创建和短状态确认。" allowed-tools: Bash, Read metadata: {"openclaw":{"emoji":"🧰","requires":{"bins":["node"],"env":["MY_MCP_TOKEN"]},"primaryEnv":"MY_MCP_TOKEN"}} --- # 小白打印助手 你是一个打印助手，帮助用户通过仓库内置 wrapper 完成文件打印。工具 schema 缓存在 `{baseDir}/skills/xiaobai-print/schema/tools.json`，实际调用统一通过 `{baseDir}/skills/xiaobai-print/scripts/invoke.js` 转发到本地 MCP bridge。 用户请求: $ARGUMENTS ## 可用工具 ### deviceReadyCheck 检查打印设备是否在线且可用；成功时通常还会返回设备状态和打印机基础信息（如名称、厂商、型号、序列号、是否单色机）。 Arguments: - none ### deviceGetCapability 获取打印设备支持的纸张尺寸、介质、彩色和双面能力。 Arguments: - none ### printCreate 创建打印任务。参数会透传到 bridge 对接的上游 `printCreate`。 Arguments: - payload (object, required): 传入上游 `printCreate` 需要的 JSON 字段，常见内容包括文件地址、文件名、打印份数、纸张尺寸、纸张类型、单双面、页码范围、奇偶页、彩色模式等。A4 图片（jpg/jpeg）默认按文档模式打印；只有在用户明确确认需要 A4 照片纸时才传 `media_type=photo` ### getCosUploadToken 获取上传凭证，通常由 `uploadFile` 间接使用。 Arguments: - none ### uploadFile 上传本地文件并返回可传给 `printCreate` 的 CDN 地址。 Arguments: - filePath (string, required): 本地文件绝对路径 - fileName (string, optional): 文件名，不提供时从路径推断 ### orderGetStatus 查询打印任务状态。 Arguments: - payload (object, required): 传入 bridge 或上游状态查询接口要求的 JSON 字段 ### orderCancel 取消打印任务。 Arguments: - payload (object, required): 传入 bridge 或上游取消接口要求的 JSON 字段 ## 调用方式 需要调用上述工具时，运行： `node {baseDir}/skills/xiaobai-print/scripts/invoke.js <tool-name> '<json-args>'` 要求： - 第二个参数必须是合法 JSON - 不要发明超出工具 schema 的字段 - 如果 skill 不可用，先配置 `skills.entries.xiaobai-print.apiKey` - 如需切换 bridge 地址，配置 `skills.entries.xiaobai-print.env.MY_MCP_BASE_URL` ## 打印工作流（严格按顺序执行） ### 第一步：检查设备状态（必须） 运行 `node {baseDir}/skills/xiaobai-print/scripts/invoke.js deviceReadyCheck '{}'` 检查打印设备是否在线且可用。 - 如果返回 `can_print !== true`，或 `box_status` / `printer_status` 明显异常，告知用户具体原因并**停止流程**，不要继续后续步骤 - 如果返回 `printer_info`，可向用户简要确认当前设备，例如设备名称、厂商、型号；排查问题时优先带上这些信息 - `printer_info.mono` 仅作为设备展示信息参考，不要用它替代 `deviceGetCapability` 的能力判断 - 如果设备**可用**，继续下一步 ### 第二步：获取打印能力 运行 `node {baseDir}/skills/xiaobai-print/scripts/invoke.js deviceGetCapability '{}'` 获取设备支持的纸张尺寸、彩色/双面/纸张类型等。 根据用户需求确认： - 用户需要的纸张尺寸是否支持 - 用户需要的介质类型（plain/photo）是否支持 - 如果不支持，告知用户设备实际支持的选项，让用户选择 常见纸张尺寸参考： | 尺寸 | 介质 | 说明 | 家庭常用 | |------|------|------|----------| | A4 | plain | A4 文档 | 是 | | A4 | photo | A4 照片 | | | A3 | plain | A3 文档 | | | A5 | plain | A5 文档 | | | B4 | plain | B4 文档 | 是 | | B5 | plain | B5 文档 | | | 5in | photo | 5 寸照片 | | | 6in | photo | 6 寸照片 | 是 | | 7in | photo | 7 寸照片 | | ### 第三步：准备文件 判断文件来源： - **网络 URL**（http/https 开头）→ 直接使用，跳到第四步 - **本地文件路径** → 运行 `node {baseDir}/skills/xiaobai-print/scripts/invoke.js uploadFile '<json-args>'` 上传文件，获取返回的 CDN 地址 ### 第四步：创建打印任务 运行 `node {baseDir}/skills/xiaobai-print/scripts/invoke.js printCreate '<json-args>'` 创建打印任务。 需确认的参数： - 文件地址（URL 或上一步的 CDN 地址） - 文件名 - 打印份数（默认 1） - 可选：纸张尺寸、纸张类型、单双面、页码范围、奇偶页、彩色模式 注意： - 设备信息会自动补全，无需手动指定 - A4 图片（jpg/jpeg）默认按文档模式打印，可理解为图片转文档；这也是大多数用户打印 A4 图片的方式 - 只有在用户明确确认需要 A4 照片纸时，才使用 `media_size=A4` 且 `media_type=photo` - 照片打印（photo 介质）会忽略单双面、页码范围、奇偶页设置 ### 第五步：短轮询确认任务已进入执行 使用 `node {baseDir}/skills/xiaobai-print/scripts/invoke.js orderGetStatus '<json-args>'` 做短状态确认： 1. 从 `printCreate` 返回中获取设计单号 2. 等待 5-10 秒后调用一次 `orderGetStatus` 3. 如果返回 `state` / `human_state` 表示“打印中 / 已接单 / 排队中 / 待打印 / 处理中”等非失败进行态，直接告知用户任务已提交成功，可稍后去打印机取件，然后结束当前流程 4. 如果返回明确失败态，告知用户失败原因 5. 只有在用户明确要求持续跟踪，或需要排查异常时，才继续每隔 10-15 秒查询一次，最多持续 5 分钟 如果打印失败，告知用户失败原因。如果用户要求取消，使用 `node {baseDir}/skills/xiaobai-print/scripts/invoke.js orderCancel '<json-args>'`。 ## 重要提醒 - **每次打印前必须执行 `deviceReadyCheck`**，即使刚检查过 - `deviceReadyCheck` 返回的 `printer_info` 适合用于展示或排障；纸张尺寸、介质、彩色、双面能力仍必须以 `deviceGetCapability` 为准 - **不要猜测设备能力**，必须通过 `deviceGetCapability` 确认 - 上传文件失败时，告知用户错误信息，不要重试超过 2 次 - **默认不要长时间占用会话轮询打印结果**，确认任务已进入执行即可结束 - 如果用户没有特别说明，A4 图片优先推荐 `A4 + plain/默认`，不要主动切到 A4 照片纸 - 如果用户没有指定纸张尺寸，根据文件类型推荐：文档默认 A4 plain，照片默认 6in photo

path: README.md
content: # xiaobai-print

This repository now centers on the wrapper-skill flow:

1. Run a local HTTP bridge
2. Read the bridge tool list
3. Generate one or more OpenClaw skill directories
4. Let each skill call the bridge through a local `invoke.js`

The repository still includes the stdio MCP server for Claude Desktop / Cursor, but the recommended OpenClaw path is:

`OpenClaw skill -> scripts/invoke.js -> local HTTP bridge -> remote MCP`

## Build

```bash
npm install
npm run build
```

## Executables

- `xiaobai-print-mcp`: stdio MCP server
- `xiaobai-print-bridge`: local HTTP bridge for generated skills
- `generate-openclaw-skill`: skill generator that reads the bridge tool list

## Run The Local Bridge

Start the local HTTP bridge on `127.0.0.1:8787`:

```bash
MY_MCP_UPSTREAM_URL="https://mcp.gongfudou.com/mcp/openclaw/sse" \
MY_MCP_TOKEN="your-token" \
xiaobai-print-bridge
```

Or use the compiled file directly:

```bash
node dist/bridge/http.js --host 127.0.0.1 --port 8787
```

Bridge endpoints:

- `GET /health`
- `GET /mcp/tools`
- `POST /mcp/tools/{toolName}`

The bridge accepts `Authorization: Bearer <token>` per request. If the request omits `Authorization`, it falls back to `MY_MCP_TOKEN` / `OPENCLAW_TOKEN`.

## Generate OpenClaw Skills

Generate a single skill from the local bridge:

```bash
generate-openclaw-skill \
  --bridge-url http://127.0.0.1:8787 \
  --out ./skill \
  --skill-name my-mcp
```

Split tools into multiple skill directories by tool-name prefix:

```bash
generate-openclaw-skill \
  --bridge-url http://127.0.0.1:8787 \
  --out ./skills \
  --skill-name my-mcp \
  --split-by prefix
```

Optional discovery auth:

```bash
generate-openclaw-skill \
  --bridge-url http://127.0.0.1:8787 \
  --out ./skill \
  --token "$MY_MCP_TOKEN"
```

Generated output for a single skill:

```text
skill/
  SKILL.md
  scripts/
    invoke.js
  schema/
    tools.json
```

Generated output for `--split-by prefix`:

```text
skills/
  schema/
    tools.json
  my-mcp-search/
    SKILL.md
    scripts/
      invoke.js
    schema/
      tools.json
  my-mcp-ticket/
    ...
```

The generated wrapper uses:

- `MY_MCP_TOKEN` for the bearer token
- `MY_MCP_BASE_URL` for the local bridge URL

If `MY_MCP_BASE_URL` is not set, the wrapper falls back to the bridge URL used during generation.

## OpenClaw Configuration

Recommended skill configuration:

```json
{
  "skills": {
    "entries": {
      "my-mcp": {
        "enabled": true,
        "apiKey": "your-token-here",
        "env": {
          "MY_MCP_BASE_URL": "http://127.0.0.1:8787"
        }
      }
    }
  }
}
```

The generated `SKILL.md` declares:

- `metadata.openclaw.requires.env = ["MY_MCP_TOKEN"]`
- `metadata.openclaw.primaryEnv = "MY_MCP_TOKEN"`

That lets OpenClaw inject `skills.entries.<skill>.apiKey` as `MY_MCP_TOKEN`.

## MCP Server Mode

The original stdio MCP server is still available:

```bash
node dist/index.js
```

Environment variables:

| Variable | Required | Description |
| --- | --- | --- |
| `MY_MCP_TOKEN` or `OPENCLAW_TOKEN` | Yes | Bearer token for the upstream remote MCP |
| `MY_MCP_UPSTREAM_URL` or `OPENCLAW_MCP_URL` | No | Upstream remote MCP endpoint URL |

## Examples

A checked-in generated example lives in `examples/search-docs-skill/`.

The bundled print skill now also follows the wrapper-based layout:

```text
skills/xiaobai-print/
  SKILL.md
  scripts/
    invoke.js
  schema/
    tools.json
```


path: package-lock.json
content: {
  "name": "xiaobai-print",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "xiaobai-print",
      "version": "1.0.0",
      "dependencies": {
        "@modelcontextprotocol/sdk": "^1.12.1"
      },
      "bin": {
        "generate-openclaw-skill": "dist/generator/cli.js",
        "xiaobai-print-bridge": "dist/bridge/http.js",
        "xiaobai-print-mcp": "dist/index.js"
      },
      "devDependencies": {
        "@types/node": "^22.0.0",
        "typescript": "^5.7.0"
      }
    },
    "node_modules/@hono/node-server": {
      "version": "1.19.11",
      "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.14.1"
      },
      "peerDependencies": {
        "hono": "^4"
      }
    },
    "node_modules/@modelcontextprotocol/sdk": {
      "version": "1.27.1",
      "resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@hono/node-server": "^1.19.9",
        "ajv": "^8.17.1",
        "ajv-formats": "^3.0.1",
        "content-type": "^1.0.5",
        "cors": "^2.8.5",
        "cross-spawn": "^7.0.5",
        "eventsource": "^3.0.2",
        "eventsource-parser": "^3.0.0",
        "express": "^5.2.1",
        "express-rate-limit": "^8.2.1",
        "hono": "^4.11.4",
        "jose": "^6.1.3",
        "json-schema-typed": "^8.0.2",
        "pkce-challenge": "^5.0.0",
        "raw-body": "^3.0.0",
        "zod": "^3.25 || ^4.0",
        "zod-to-json-schema": "^3.25.1"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@cfworker/json-schema": "^4.1.1",
        "zod": "^3.25 || ^4.0"
      },
      "peerDependenciesMeta": {
        "@cfworker/json-schema": {
          "optional": true
        },
        "zod": {
          "optional": false
        }
      }
    },
    "node_modules/@types/node": {
      "version": "22.19.15",
      "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.15.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/accepts": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "^3.0.0",
        "negotiator": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/ajv": {
      "version": "8.18.0",
      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fast-deep-equal": "^3.1.3",
        "fast-uri": "^3.0.1",
        "json-schema-traverse": "^1.0.0",
        "require-from-string": "^2.0.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/ajv-formats": {
      "version": "3.0.1",
      "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependenciesMeta": {
        "ajv": {
          "optional": true
        }
      }
    },
    "node_modules/body-parser": {
      "version": "2.2.2",
      "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "^3.1.2",
        "content-type": "^1.0.5",
        "debug": "^4.4.3",
        "http-errors": "^2.0.0",
        "iconv-lite": "^0.7.0",
        "on-finished": "^2.4.1",
        "qs": "^6.14.1",
        "raw-body": "^3.0.1",
        "type-is": "^2.0.1"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/content-disposition": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.2.2",
      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6.6.0"
      }
    },
    "node_modules/cors": {
      "version": "2.8.6",
      "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "object-assign": "^4",
        "vary": "^1"
      },
      "engines": {
        "node": ">= 0.10"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/eventsource": {
      "version": "3.0.7",
      "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "eventsource-parser": "^3.0.1"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/eventsource-parser": {
      "version": "3.0.6",
      "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/express": {
      "version": "5.2.1",
      "resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "accepts": "^2.0.0",
        "body-parser": "^2.2.1",
        "content-disposition": "^1.0.0",
        "content-type": "^1.0.5",
        "cookie": "^0.7.1",
        "cookie-signature": "^1.2.1",
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "finalhandler": "^2.1.0",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.0",
        "merge-descriptors": "^2.0.0",
        "mime-types": "^3.0.0",
        "on-finished": "^2.4.1",
        "once": "^1.4.0",
        "parseurl": "^1.3.3",
        "proxy-addr": "^2.0.7",
        "qs": "^6.14.0",
        "range-parser": "^1.2.1",
        "router": "^2.2.0",
        "send": "^1.1.0",
        "serve-static": "^2.2.0",
        "statuses": "^2.0.1",
        "type-is": "^2.0.1",
        "vary": "^1.1.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/express-rate-limit": {
      "version": "8.3.1",
      "resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ip-address": "10.1.0"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/express-rate-limit"
      },
      "peerDependencies": {
        "express": ">= 4.11"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fast-uri": {
      "version": "3.1.0",
      "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fastify"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/fastify"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/finalhandler": {
      "version": "2.1.1",
      "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "on-finished": "^2.4.1",
        "parseurl": "^1.3.3",
        "statuses": "^2.0.1"
      },
      "engines": {
        "node": ">= 18.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/hono": {
      "version": "4.12.7",
      "resolved": "https://registry.npmmirror.com/hono/-/hono-4.12.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.9.0"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.7.2",
      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ip-address": {
      "version": "10.1.0",
      "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-promise": {
      "version": "4.0.0",
      "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/jose": {
      "version": "6.2.1",
      "resolved": "https://registry.npmmirror.com/jose/-/jose-6.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/panva"
      }
    },
    "node_modules/json-schema-traverse": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/json-schema-typed": {
      "version": "8.0.2",
      "resolved": "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "1.1.0",
      "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/mime-db": {
      "version": "1.54.0",
      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "3.0.2",
      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "^1.54.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/negotiator": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/object-assign": {
      "version": "4.1.1",
      "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "8.3.0",
      "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/pkce-challenge": {
      "version": "5.0.1",
      "resolved": "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.20.0"
      }
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/qs": {
      "version": "6.15.0",
      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "3.0.2",
      "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.7.0",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/require-from-string": {
      "version": "2.0.2",
      "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/router": {
      "version": "2.2.0",
      "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "is-promise": "^4.0.0",
        "parseurl": "^1.3.3",
        "path-to-regexp": "^8.0.0"
      },
      "engines": {
        "node": ">= 18"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "1.2.1",
      "resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.3",
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "etag": "^1.8.1",
        "fresh": "^2.0.0",
        "http-errors": "^2.0.1",
        "mime-types": "^3.0.2",
        "ms": "^2.1.3",
        "on-finished": "^2.4.1",
        "range-parser": "^1.2.1",
        "statuses": "^2.0.2"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/serve-static": {
      "version": "2.2.1",
      "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "parseurl": "^1.3.3",
        "send": "^1.2.0"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/type-is": {
      "version": "2.0.1",
      "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "content-type": "^1.0.5",
        "media-typer": "^1.1.0",
        "mime-types": "^3.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/zod": {
      "version": "4.3.6",
      "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/colinhacks"
      }
    },
    "node_modules/zod-to-json-schema": {
      "version": "3.25.1",
      "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "peerDependencies": {
        "zod": "^3.25 || ^4"
      }
    }
  }
}


path: package.json
content: {
  "name": "xiaobai-print",
  "version": "1.0.0",
  "description": "MCP print integration with bundled skills and an OpenClaw skill generator",
  "type": "module",
  "bin": {
    "xiaobai-print-mcp": "dist/index.js",
    "xiaobai-print-bridge": "dist/bridge/http.js",
    "generate-openclaw-skill": "dist/generator/cli.js"
  },
  "files": [
    "dist",
    "skills",
    "README.md",
    "LICENSE"
  ],
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "prepublishOnly": "npm run build"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.7.0"
  }
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src"]
}


path: src/index.ts
content: import "./mcp/index.js";


path: src/generator/cli.ts
content: #!/usr/bin/env node
import path from "node:path";
import { writeOpenClawSkills } from "./generate.js";

interface CliOptions {
  bridgeBaseUrl?: string;
  outputDir?: string;
  token?: string;
  splitBy?: "none" | "prefix";
  skillName?: string;
  homepage?: string;
  help?: boolean;
}

function usage(): string {
  return [
    "Usage:",
    "  generate-openclaw-skill --bridge-url <bridge-url> --out <output-dir>",
    "",
    "Options:",
    "  --bridge-url <url>  Local HTTP bridge base URL, for example http://127.0.0.1:8787",
    "  --mcp <url>         Deprecated alias for --bridge-url",
    "  --out <dir>         Output directory for the generated OpenClaw skill package",
    "  --skill-name <name> Base skill name. Recommended when --split-by prefix is enabled",
    "  --split-by <mode>   Skill splitting strategy: none (default) or prefix",
    "  --homepage <url>    Optional homepage written into metadata.openclaw.homepage",
    "  --token <token>     Optional bearer token used when fetching tools from the bridge",
    "  --help              Show this help message",
    "",
    "Environment:",
    "  MY_MCP_TOKEN        Optional bearer token used for discovery if --token is not provided",
  ].join("\n");
}

function readValue(argv: string[], index: number, flag: string): string {
  const value = argv[index + 1];
  if (!value || value.startsWith("--")) {
    throw new Error(`Missing value for ${flag}`);
  }
  return value;
}

function parseSplitBy(value: string): "none" | "prefix" {
  if (value === "none" || value === "prefix") {
    return value;
  }

  throw new Error(`Unsupported --split-by value: ${value}`);
}

function parseArgs(argv: string[]): CliOptions {
  const options: CliOptions = {};

  for (let index = 0; index < argv.length; index += 1) {
    const current = argv[index];

    if (current === "--help" || current === "-h") {
      options.help = true;
      continue;
    }

    if (current === "--bridge-url" || current === "--mcp") {
      options.bridgeBaseUrl = readValue(argv, index, current);
      index += 1;
      continue;
    }

    if (current.startsWith("--bridge-url=")) {
      options.bridgeBaseUrl = current.slice("--bridge-url=".length);
      continue;
    }

    if (current.startsWith("--mcp=")) {
      options.bridgeBaseUrl = current.slice("--mcp=".length);
      continue;
    }

    if (current === "--out") {
      options.outputDir = readValue(argv, index, current);
      index += 1;
      continue;
    }

    if (current.startsWith("--out=")) {
      options.outputDir = current.slice("--out=".length);
      continue;
    }

    if (current === "--token") {
      options.[REDACTED_SECRET], index, current);
      index += 1;
      continue;
    }

    if (current.startsWith("--token=")) {
      options.[REDACTED_SECRET]"--[REDACTED_SECRET]);
      continue;
    }

    if (current === "--skill-name") {
      options.skillName = readValue(argv, index, current);
      index += 1;
      continue;
    }

    if (current.startsWith("--skill-name=")) {
      options.skillName = current.slice("--skill-name=".length);
      continue;
    }

    if (current === "--split-by") {
      options.splitBy = parseSplitBy(readValue(argv, index, current));
      index += 1;
      continue;
    }

    if (current.startsWith("--split-by=")) {
      options.splitBy = parseSplitBy(current.slice("--split-by=".length));
      continue;
    }

    if (current === "--homepage") {
      options.homepage = readValue(argv, index, current);
      index += 1;
      continue;
    }

    if (current.startsWith("--homepage=")) {
      options.homepage = current.slice("--homepage=".length);
      continue;
    }

    throw new Error(`Unknown argument: ${current}`);
  }

  return options;
}

async function main() {
  const options = parseArgs(process.argv.slice(2));

  if (options.help) {
    console.log(usage());
    return;
  }

  if (!options.bridgeBaseUrl || !options.outputDir) {
    throw new Error(`Both --bridge-url and --out are required.\n\n${usage()}`);
  }

  const bundle = await writeOpenClawSkills({
    bridgeBaseUrl: options.bridgeBaseUrl,
    outputDir: options.outputDir,
    [REDACTED_SECRET] ?? process.env.MY_MCP_TOKEN ?? process.env.OPENCLAW_TOKEN,
    splitBy: options.splitBy ?? "none",
    skillName: options.skillName,
    homepage: options.homepage,
  });

  const lines = [
    `[generate-openclaw-skill] Generated ${bundle.skills.length} skill package${bundle.skills.length === 1 ? "" : "s"}`,
    `Bridge: ${bundle.resolvedUrls.baseUrl}`,
    `Output root: ${path.resolve(bundle.rootOutputDir)}`,
    `Split mode: ${bundle.splitBy}`,
    ...bundle.skills.map((skill) => `- ${skill.skillName}: ${skill.tools.length} tools -> ${skill.outputDir}`),
  ];

  process.stderr.write(`${lines.join("\n")}\n`);
}

main().catch((error) => {
  const message = error instanceof Error ? error.message : String(error);
  process.stderr.write(`[generate-openclaw-skill] ${message}\n`);
  process.exit(1);
});


path: src/generator/types.ts
content: export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };

export interface JsonSchema {
  type?: string | string[];
  title?: string;
  description?: string;
  properties?: Record<string, JsonSchema>;
  required?: string[];
  additionalProperties?: boolean | JsonSchema;
  items?: JsonSchema | JsonSchema[];
  enum?: JsonPrimitive[];
  const?: JsonPrimitive;
  default?: JsonValue;
  examples?: JsonValue[];
  format?: string;
  nullable?: boolean;
  oneOf?: JsonSchema[];
  anyOf?: JsonSchema[];
  allOf?: JsonSchema[];
}

export interface McpToolDefinition {
  name: string;
  description: string;
  inputSchema: JsonSchema;
}

export interface GeneratorOptions {
  bridgeBaseUrl: string;
  outputDir: string;
  token?: string;
  splitBy?: "none" | "prefix";
  skillName?: string;
  homepage?: string;
}

export interface ResolvedBridgeUrls {
  rawInputUrl: string;
  baseUrl: string;
  toolCatalogUrl: string;
  toolInvokeBaseUrl: string;
}

export interface ToolArgumentSummary {
  name: string;
  type: string;
  required: boolean;
  description?: string;
}

export interface GeneratedSkill {
  skillName: string;
  description: string;
  tools: McpToolDefinition[];
  skillMarkdown: string;
  invokeScript: string;
  toolsSchemaJson: string;
  resolvedUrls: ResolvedBridgeUrls;
  outputDir: string;
  groupKey: string;
}

export interface GeneratedSkillBundle {
  rootOutputDir: string;
  resolvedUrls: ResolvedBridgeUrls;
  splitBy: "none" | "prefix";
  allTools: McpToolDefinition[];
  allToolsSchemaJson: string;
  skills: GeneratedSkill[];
}


path: src/generator/generate.ts
content: import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { renderAllToolsSchemaJson, renderInvokeScript, renderSkillMarkdown, renderToolsSchemaJson } from "./render.js";
import type {
  GeneratedSkill,
  GeneratedSkillBundle,
  GeneratorOptions,
  JsonSchema,
  McpToolDefinition,
  ResolvedBridgeUrls,
} from "./types.js";

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function formatError(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function normalizeUrl(input: string): URL {
  try {
    return new URL(input);
  } catch (error) {
    throw new Error(`Invalid bridge URL "${input}": ${formatError(error)}`);
  }
}

function joinUrl(origin: string, pathname: string): string {
  return pathname ? `${origin}${pathname}` : origin;
}

export function resolveBridgeUrls(rawInputUrl: string): ResolvedBridgeUrls {
  const url = normalizeUrl(rawInputUrl);
  const trimmedPath = url.pathname.replace(/\/+$/, "");

  let bridgePath = trimmedPath;
  if (bridgePath.endsWith("/mcp/tools")) {
    bridgePath = bridgePath.slice(0, -"/mcp/tools".length);
  } else if (bridgePath.endsWith("/mcp")) {
    bridgePath = bridgePath.slice(0, -"/mcp".length);
  }

  const baseUrl = joinUrl(url.origin, bridgePath);

  return {
    rawInputUrl,
    baseUrl,
    toolCatalogUrl: `${baseUrl}/mcp/tools`,
    toolInvokeBaseUrl: `${baseUrl}/mcp/tools`,
  };
}

function toKebabCase(value: string): string {
  return value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

function deriveBaseSkillName(outputDir: string, explicitSkillName?: string): string {
  if (explicitSkillName) {
    return toKebabCase(explicitSkillName) || "generated-mcp-skill";
  }

  const folderName = path.basename(path.resolve(outputDir));
  return toKebabCase(folderName) || "generated-mcp-skill";
}

async function fetchJson(url: string, init?: RequestInit): Promise<unknown> {
  const response = await fetch(url, init);
  const body = await response.text();

  if (!response.ok) {
    const message = body.trim() || response.statusText;
    throw new Error(`${response.status} ${response.statusText}: ${message}`);
  }

  try {
    return body ? JSON.parse(body) : null;
  } catch (error) {
    throw new Error(`Expected JSON response from ${url}: ${formatError(error)}`);
  }
}

function extractTools(payload: unknown): unknown[] {
  if (Array.isArray(payload)) {
    return payload;
  }

  if (isObject(payload) && Array.isArray(payload.tools)) {
    return payload.tools;
  }

  throw new Error("Bridge response does not contain a tools array.");
}

function normalizeSchema(schema: unknown): JsonSchema {
  if (!isObject(schema)) {
    return {
      type: "object",
      additionalProperties: true,
      description: "No input schema was returned by the bridge.",
    };
  }

  return schema as JsonSchema;
}

function normalizeTool(tool: unknown, index: number): McpToolDefinition {
  if (!isObject(tool)) {
    throw new Error(`Tool at index ${index} is not an object.`);
  }

  if (typeof tool.name !== "string" || tool.name.trim() === "") {
    throw new Error(`Tool at index ${index} is missing a valid name.`);
  }

  return {
    name: tool.name.trim(),
    description:
      typeof tool.description === "string" && tool.description.trim() !== ""
        ? tool.description.trim()
        : `MCP tool ${tool.name.trim()}.`,
    inputSchema: normalizeSchema(tool.inputSchema),
  };
}

async function fetchBridgeTools(resolvedUrls: ResolvedBridgeUrls, token?: string): Promise<McpToolDefinition[]> {
  const headers = new Headers({ Accept: "application/json" });
  if (token) {
    headers.set("Authorization", `Bearer ${token}`);
  }

  const payload = await fetchJson(resolvedUrls.toolCatalogUrl, {
    method: "GET",
    headers,
  });

  const tools = extractTools(payload).map(normalizeTool).sort((left, right) => left.name.localeCompare(right.name));
  if (tools.length === 0) {
    throw new Error("Bridge returned an empty tool list.");
  }

  return tools;
}

function groupKeyFromToolName(name: string): string {
  if (name.includes("_") || name.includes("-")) {
    const prefix = name.split(/[_-]/).find(Boolean);
    return toKebabCase(prefix ?? "general") || "general";
  }

  const camelPrefix = name.match(/^[A-Z]?[a-z]+/)?.[0];
  return toKebabCase(camelPrefix ?? name) || "general";
}

function groupTools(
  tools: McpToolDefinition[],
  splitBy: "none" | "prefix",
): Array<{ groupKey: string; tools: McpToolDefinition[] }> {
  if (splitBy === "none") {
    return [{ groupKey: "all", tools }];
  }

  const groups = new Map<string, McpToolDefinition[]>();
  for (const tool of tools) {
    const groupKey = groupKeyFromToolName(tool.name);
    const existing = groups.get(groupKey) ?? [];
    existing.push(tool);
    groups.set(groupKey, existing);
  }

  return [...groups.entries()]
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([groupKey, groupedTools]) => ({
      groupKey,
      tools: groupedTools.sort((left, right) => left.name.localeCompare(right.name)),
    }));
}

function describeSkill(
  baseSkillName: string,
  groupKey: string,
  tools: McpToolDefinition[],
  splitBy: "none" | "prefix",
): { skillName: string; description: string } {
  if (splitBy === "none") {
    return {
      skillName: baseSkillName,
      description: `Use this skill when the user needs capabilities exposed by the local MCP bridge. It wraps ${tools.length} MCP tool${tools.length === 1 ? "" : "s"} behind a local Node.js wrapper.`,
    };
  }

  return {
    skillName: `${baseSkillName}-${groupKey}`,
    description: `Use this skill when the user needs ${groupKey}-related capabilities exposed by the local MCP bridge. It wraps ${tools.length} MCP tool${tools.length === 1 ? "" : "s"} behind a local Node.js wrapper.`,
  };
}

function buildSkillOutputDir(
  rootOutputDir: string,
  splitBy: "none" | "prefix",
  skillName: string,
): string {
  if (splitBy === "none") {
    return rootOutputDir;
  }

  return path.join(rootOutputDir, skillName);
}

export async function generateOpenClawSkills(options: GeneratorOptions): Promise<GeneratedSkillBundle> {
  const splitBy = options.splitBy ?? "none";
  const resolvedUrls = resolveBridgeUrls(options.bridgeBaseUrl);
  const tools = await fetchBridgeTools(resolvedUrls, options.token);
  const baseSkillName = deriveBaseSkillName(options.outputDir, options.skillName);
  const groups = groupTools(tools, splitBy);
  const rootOutputDir = path.resolve(options.outputDir);

  const skills: GeneratedSkill[] = groups.map(({ groupKey, tools: groupedTools }) => {
    const { skillName, description } = describeSkill(baseSkillName, groupKey, groupedTools, splitBy);
    return {
      skillName,
      description,
      tools: groupedTools,
      skillMarkdown: renderSkillMarkdown({
        skillName,
        description,
        tools: groupedTools,
        resolvedUrls,
        homepage: options.homepage,
      }),
      invokeScript: renderInvokeScript(resolvedUrls),
      toolsSchemaJson: renderToolsSchemaJson({
        skillName,
        description,
        resolvedUrls,
        tools: groupedTools,
      }),
      resolvedUrls,
      outputDir: buildSkillOutputDir(rootOutputDir, splitBy, skillName),
      groupKey,
    };
  });

  const bundle: GeneratedSkillBundle = {
    rootOutputDir,
    resolvedUrls,
    splitBy,
    allTools: tools,
    allToolsSchemaJson: "",
    skills,
  };

  bundle.allToolsSchemaJson = renderAllToolsSchemaJson(bundle);
  return bundle;
}

async function writeSkillPackage(skill: GeneratedSkill) {
  const scriptsDir = path.join(skill.outputDir, "scripts");
  const schemaDir = path.join(skill.outputDir, "schema");

  await mkdir(scriptsDir, { recursive: true });
  await mkdir(schemaDir, { recursive: true });

  await Promise.all([
    writeFile(path.join(skill.outputDir, "SKILL.md"), skill.skillMarkdown, "utf8"),
    writeFile(path.join(scriptsDir, "invoke.js"), skill.invokeScript, {
      encoding: "utf8",
      mode: 0o755,
    }),
    writeFile(path.join(schemaDir, "tools.json"), skill.toolsSchemaJson, "utf8"),
  ]);
}

export async function writeOpenClawSkills(options: GeneratorOptions): Promise<GeneratedSkillBundle> {
  const bundle = await generateOpenClawSkills(options);

  await Promise.all(bundle.skills.map((skill) => writeSkillPackage(skill)));

  if (bundle.splitBy !== "none") {
    const rootSchemaDir = path.join(bundle.rootOutputDir, "schema");
    await mkdir(rootSchemaDir, { recursive: true });
    await writeFile(path.join(rootSchemaDir, "tools.json"), bundle.allToolsSchemaJson, "utf8");
  }

  return bundle;
}


path: src/generator/render.ts
content: import type {
  GeneratedSkillBundle,
  JsonPrimitive,
  JsonSchema,
  JsonValue,
  McpToolDefinition,
  ResolvedBridgeUrls,
  ToolArgumentSummary,
} from "./types.js";

function formatJsonValue(value: JsonPrimitive | JsonValue): string {
  return JSON.stringify(value);
}

function getSchemaTypes(schema: JsonSchema): string[] {
  if (Array.isArray(schema.type)) {
    return schema.type;
  }

  if (schema.type) {
    return [schema.type];
  }

  if (schema.oneOf?.length) {
    return schema.oneOf.flatMap(getSchemaTypes);
  }

  if (schema.anyOf?.length) {
    return schema.anyOf.flatMap(getSchemaTypes);
  }

  if (schema.allOf?.length) {
    return schema.allOf.flatMap(getSchemaTypes);
  }

  if (schema.properties || schema.additionalProperties !== undefined) {
    return ["object"];
  }

  if (schema.items) {
    return ["array"];
  }

  return ["unknown"];
}

function getArrayItemSchema(items: JsonSchema | JsonSchema[] | undefined): JsonSchema | undefined {
  if (!items) {
    return undefined;
  }

  return Array.isArray(items) ? items[0] : items;
}

export function describeSchemaType(schema: JsonSchema): string {
  if (schema.const !== undefined) {
    return `const ${formatJsonValue(schema.const)}`;
  }

  if (schema.enum?.length) {
    const values = schema.enum.map(formatJsonValue).join(", ");
    return `${getSchemaTypes(schema).join(" | ")} (one of: ${values})`;
  }

  if (schema.oneOf?.length) {
    return schema.oneOf.map(describeSchemaType).join(" | ");
  }

  if (schema.anyOf?.length) {
    return schema.anyOf.map(describeSchemaType).join(" | ");
  }

  if (schema.allOf?.length) {
    return schema.allOf.map(describeSchemaType).join(" & ");
  }

  const normalizedTypes = getSchemaTypes(schema).map((type) => {
    if (type === "array") {
      const itemSchema = getArrayItemSchema(schema.items);
      return itemSchema ? `array<${describeSchemaType(itemSchema)}>` : "array<unknown>";
    }

    if (type === "object" && schema.additionalProperties && !schema.properties) {
      return "object<string, unknown>";
    }

    return type;
  });

  const uniqueTypes = [...new Set(normalizedTypes)];

  if (schema.nullable && !uniqueTypes.includes("null")) {
    uniqueTypes.push("null");
  }

  return uniqueTypes.join(" | ");
}

export function summarizeToolArguments(schema: JsonSchema): ToolArgumentSummary[] {
  const properties = schema.properties ?? {};
  const required = new Set(schema.required ?? []);

  return Object.entries(properties).map(([name, propertySchema]) => ({
    name,
    type: describeSchemaType(propertySchema),
    required: required.has(name),
    description: propertySchema.description?.trim(),
  }));
}

function renderArgumentLines(tool: McpToolDefinition): string {
  const argumentsSummary = summarizeToolArguments(tool.inputSchema);
  if (argumentsSummary.length === 0) {
    return "- none";
  }

  return argumentsSummary
    .map((argument) => {
      const suffix = argument.description ? `: ${argument.description}` : "";
      return `- ${argument.name} (${argument.type}, ${argument.required ? "required" : "optional"})${suffix}`;
    })
    .join("\n");
}

function escapeFrontmatterString(value: string): string {
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

function renderToolSection(tool: McpToolDefinition): string {
  return [
    `### ${tool.name}`,
    tool.description || "No description provided.",
    "",
    "Arguments:",
    renderArgumentLines(tool),
    "",
  ].join("\n");
}

export function renderSkillMarkdown(input: {
  skillName: string;
  description: string;
  tools: McpToolDefinition[];
  resolvedUrls: ResolvedBridgeUrls;
  homepage?: string;
}): string {
  const metadata = JSON.stringify({
    openclaw: {
      emoji: "🧰",
      requires: {
        bins: ["node"],
        env: ["MY_MCP_TOKEN"],
      },
      primaryEnv: "MY_MCP_TOKEN",
      ...(input.homepage ? { homepage: input.homepage } : {}),
    },
  });

  return [
    "---",
    `name: ${input.skillName}`,
    `description: "${escapeFrontmatterString(input.description)}"`,
    "allowed-tools: Bash, Read",
    `metadata: ${metadata}`,
    "---",
    "",
    `# ${input.skillName}`,
    "",
    input.description,
    "",
    `This skill targets the local MCP bridge at \`${input.resolvedUrls.baseUrl}\` by default. Tool schemas are cached in \`{baseDir}/schema/tools.json\`.`,
    "",
    "## Available tools",
    "",
    ...input.tools.map((tool) => renderToolSection(tool)),
    "## How to invoke",
    "",
    "When you need one of the tools above, run:",
    "",
    "`node {baseDir}/scripts/invoke.js <tool-name> '<json-args>'`",
    "",
    "Pass valid JSON as the second argument.",
    "Do not invent fields not present in the tool schema.",
    "If the skill is unavailable, configure `skills.entries." + input.skillName + ".apiKey` first.",
    "Optionally set `skills.entries." + input.skillName + ".env.MY_MCP_BASE_URL` to point at your local bridge.",
    "",
  ].join("\n");
}

export function renderInvokeScript(resolvedUrls: ResolvedBridgeUrls): string {
  return `#!/usr/bin/env node
const DEFAULT_BASE_URL = ${JSON.stringify(resolvedUrls.baseUrl)};

function normalizeBaseUrl(value) {
  return value.replace(/\\/+$/, "");
}

async function main() {
  const [, , toolName, rawArgs] = process.argv;

  if (!toolName) {
    console.error("Missing tool name");
    process.exit(1);
  }

  let args = {};
  if (rawArgs) {
    try {
      args = JSON.parse(rawArgs);
    } catch (error) {
      console.error(\`Invalid JSON arguments: \${error instanceof Error ? error.message : String(error)}\`);
      process.exit(4);
    }
  }

  if (!args || typeof args !== "object" || Array.isArray(args)) {
    console.error("Arguments must be a JSON object");
    process.exit(4);
  }

  const [REDACTED_SECRET];
  if (!token) {
    console.error("Missing MY_MCP_TOKEN");
    process.exit(2);
  }

  const baseUrl = normalizeBaseUrl(process.env.MY_MCP_BASE_URL || DEFAULT_BASE_URL || "http://127.0.0.1:8787");
  const response = await fetch(\`\${baseUrl}/mcp/tools/\${encodeURIComponent(toolName)}\`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: \`Bearer \${token}\`,
    },
    body: JSON.stringify({ arguments: args }),
  });

  const text = await response.text();
  if (!response.ok) {
    console.error(text || \`Request failed: \${response.status} \${response.statusText}\`);
    process.exit(3);
  }

  process.stdout.write(text.endsWith("\\n") ? text : \`\${text}\\n\`);
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack || error.message : String(error));
  process.exit(10);
});
`;
}

export function renderToolsSchemaJson(input: {
  skillName: string;
  description: string;
  resolvedUrls: ResolvedBridgeUrls;
  tools: McpToolDefinition[];
}): string {
  return `${JSON.stringify(
    {
      generatedAt: new Date().toISOString(),
      skillName: input.skillName,
      description: input.description,
      bridge: input.resolvedUrls,
      tools: input.tools,
    },
    null,
    2,
  )}\n`;
}

export function renderAllToolsSchemaJson(bundle: GeneratedSkillBundle): string {
  return `${JSON.stringify(
    {
      generatedAt: new Date().toISOString(),
      splitBy: bundle.splitBy,
      bridge: bundle.resolvedUrls,
      skills: bundle.skills.map((skill) => ({
        skillName: skill.skillName,
        groupKey: skill.groupKey,
        outputDir: skill.outputDir,
        toolCount: skill.tools.length,
      })),
      tools: bundle.allTools,
    },
    null,
    2,
  )}\n`;
}


path: src/mcp/index.ts
content: import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  GetPromptRequestSchema,
  ListPromptsRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { resolveConfig } from "../core/config.js";
import { invokeExposedTool, listExposedTools } from "../core/proxy-tools.js";
import { PRINT_INSTRUCTIONS, PRINT_PROMPT_NAME } from "../core/tool-specs.js";

function log(...args: unknown[]) {
  console.error("[xiaobai-print]", ...args);
}

async function main() {
  const { token, remoteUrl } = resolveConfig();
  if (!token) {
    log("ERROR: OPENCLAW_TOKEN environment variable is required");
    process.exit(1);
  }

  log(`Connecting to remote MCP server: ${remoteUrl}`);
  const allTools = await listExposedTools();
  log(`Discovered ${allTools.length} exposed tools`);

  const server = new Server(
    { name: "xiaobai-print-mcp", version: "1.0.0" },
    { capabilities: { tools: {}, prompts: {} } },
  );

  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: allTools,
  }));

  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    log(`Tool call: ${name}`);
    return await invokeExposedTool(name, args);
  });

  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
    prompts: [
      {
        name: PRINT_PROMPT_NAME,
        description: "小白打印助手使用指南 — 打印工作流、纸张尺寸、注意事项",
      },
    ],
  }));

  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
    if (request.params.name !== PRINT_PROMPT_NAME) {
      throw new Error(`Unknown prompt: ${request.params.name}`);
    }

    return {
      description: "小白打印助手使用指南",
      messages: [
        {
          role: "user" as const,
          content: { type: "text" as const, text: PRINT_INSTRUCTIONS },
        },
      ],
    };
  });

  const transport = new StdioServerTransport();
  await server.connect(transport);
  log("Local MCP server running on stdio");
}

main().catch((error) => {
  log("Fatal error:", error);
  process.exit(1);
});


path: src/bridge/http.ts
content: #!/usr/bin/env node
import http from "node:http";
import { resolveConfig } from "../core/config.js";
import { invokeExposedTool, listExposedTools } from "../core/proxy-tools.js";
import type { RemoteConfig, ToolInvocationResult } from "../core/types.js";

interface BridgeCliOptions {
  host: string;
  port: number;
  remoteUrl?: string;
}

function log(...args: unknown[]) {
  console.error("[xiaobai-print-bridge]", ...args);
}

function usage(): string {
  return [
    "Usage:",
    "  xiaobai-print-bridge [--host 127.0.0.1] [--port 8787] [--remote-url <upstream-mcp-url>]",
    "",
    "Environment:",
    "  MY_MCP_UPSTREAM_URL / OPENCLAW_MCP_URL  Upstream remote MCP endpoint URL",
    "  MY_MCP_TOKEN / OPENCLAW_TOKEN           Default upstream bearer token if the request omits Authorization",
  ].join("\n");
}

function parseArgs(argv: string[]): BridgeCliOptions {
  const options: BridgeCliOptions = {
    host: "127.0.0.1",
    port: 8787,
  };

  for (let index = 0; index < argv.length; index += 1) {
    const current = argv[index];
    const next = argv[index + 1];

    if (current === "--help" || current === "-h") {
      console.log(usage());
      process.exit(0);
    }

    if (current === "--host") {
      if (!next) {
        throw new Error("Missing value for --host");
      }
      options.host = next;
      index += 1;
      continue;
    }

    if (current.startsWith("--host=")) {
      options.host = current.slice("--host=".length);
      continue;
    }

    if (current === "--port") {
      if (!next) {
        throw new Error("Missing value for --port");
      }
      const port = Number(next);
      if (!Number.isInteger(port) || port <= 0) {
        throw new Error(`Invalid port: ${next}`);
      }
      options.port = port;
      index += 1;
      continue;
    }

    if (current.startsWith("--port=")) {
      const port = Number(current.slice("--port=".length));
      if (!Number.isInteger(port) || port <= 0) {
        throw new Error(`Invalid port: ${current.slice("--port=".length)}`);
      }
      options.port = port;
      continue;
    }

    if (current === "--remote-url") {
      if (!next) {
        throw new Error("Missing value for --remote-url");
      }
      options.remoteUrl = next;
      index += 1;
      continue;
    }

    if (current.startsWith("--remote-url=")) {
      options.remoteUrl = current.slice("--remote-url=".length);
      continue;
    }

    throw new Error(`Unknown argument: ${current}`);
  }

  return options;
}

function writeJson(res: http.ServerResponse, status: number, payload: unknown) {
  res.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
  });
  res.end(`${JSON.stringify(payload, null, 2)}\n`);
}

function extractBearerToken(req: http.IncomingMessage): string | undefined {
  const [REDACTED_SECRET];
  if (!authorization) {
    return undefined;
  }

  const match = authorization.match(/^Bearer\s+(.+)$/i);
  return match?.[1]?.trim() || undefined;
}

async function readJsonBody(req: http.IncomingMessage): Promise<unknown> {
  const chunks: Buffer[] = [];

  for await (const chunk of req) {
    chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
  }

  if (chunks.length === 0) {
    return {};
  }

  const rawBody = Buffer.concat(chunks).toString("utf8").trim();
  if (!rawBody) {
    return {};
  }

  try {
    return JSON.parse(rawBody);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`Invalid JSON body: ${message}`);
  }
}

function normalizeRequestUrl(req: http.IncomingMessage, options: BridgeCliOptions): URL {
  const host = req.headers.host ?? `${options.host}:${options.port}`;
  return new URL(req.url ?? "/", `http://${host}`);
}

function getRemoteConfig(req: http.IncomingMessage, options: BridgeCliOptions): RemoteConfig {
  const resolved = resolveConfig({
    remoteUrl: options.remoteUrl,
  });

  return {
    [REDACTED_SECRET]) ?? resolved.token,
    remoteUrl: resolved.remoteUrl,
  };
}

function extractResultMessage(result: ToolInvocationResult): string {
  for (const item of result.content) {
    if (item && typeof item === "object" && item.type === "text" && typeof item.text === "string") {
      return item.text;
    }
  }

  return "";
}

function statusFromErrorResult(result: ToolInvocationResult): number {
  const message = extractResultMessage(result).toLowerCase();

  if (message.includes("unknown tool")) {
    return 404;
  }

  if (message.includes("required") || message.includes("invalid json")) {
    return 400;
  }

  if (message.includes("token") || message.includes("unauthorized") || message.includes("authentication")) {
    return 401;
  }

  return 502;
}

async function handleToolInvocation(
  req: http.IncomingMessage,
  res: http.ServerResponse,
  toolName: string,
  options: BridgeCliOptions,
) {
  const body = await readJsonBody(req);
  if (!body || typeof body !== "object" || Array.isArray(body)) {
    writeJson(res, 400, { error: "Request body must be a JSON object." });
    return;
  }

  const argsValue = "arguments" in body ? (body as { arguments?: unknown }).arguments : {};
  if (!argsValue || typeof argsValue !== "object" || Array.isArray(argsValue)) {
    writeJson(res, 400, { error: "The \"arguments\" field must be a JSON object." });
    return;
  }

  const result = await invokeExposedTool(toolName, argsValue as Record<string, unknown>, getRemoteConfig(req, options));

  if (result.isError) {
    writeJson(res, statusFromErrorResult(result), result);
    return;
  }

  writeJson(res, 200, result);
}

async function requestHandler(
  req: http.IncomingMessage,
  res: http.ServerResponse,
  options: BridgeCliOptions,
) {
  try {
    const url = normalizeRequestUrl(req, options);

    if (req.method === "GET" && url.pathname === "/health") {
      writeJson(res, 200, {
        ok: true,
        bridge: "xiaobai-print-bridge",
        baseUrl: `http://${options.host}:${options.port}`,
      });
      return;
    }

    if (req.method === "GET" && url.pathname === "/mcp/tools") {
      const tools = await listExposedTools(getRemoteConfig(req, options));
      writeJson(res, 200, { tools });
      return;
    }

    if (req.method === "POST" && url.pathname.startsWith("/mcp/tools/")) {
      const toolName = decodeURIComponent(url.pathname.slice("/mcp/tools/".length));
      if (!toolName) {
        writeJson(res, 400, { error: "Tool name is required." });
        return;
      }

      await handleToolInvocation(req, res, toolName, options);
      return;
    }

    writeJson(res, 404, { error: "Not found." });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log(`Request failed: ${message}`);
    const status = message.toLowerCase().includes("token") ? 401 : 500;
    writeJson(res, status, { error: message });
  }
}

async function main() {
  const options = parseArgs(process.argv.slice(2));
  const server = http.createServer((req, res) => {
    void requestHandler(req, res, options);
  });

  await new Promise<void>((resolve, reject) => {
    server.once("error", reject);
    server.listen(options.port, options.host, () => {
      server.off("error", reject);
      resolve();
    });
  });

  const resolved = resolveConfig({ remoteUrl: options.remoteUrl });
  log(`Listening on http://${options.host}:${options.port}`);
  log(`Upstream MCP: ${resolved.remoteUrl}`);
}

main().catch((error) => {
  const message = error instanceof Error ? error.message : String(error);
  log(`Fatal error: ${message}`);
  process.exit(1);
});


path: src/core/upload-file.ts
content: import { readFile } from "node:fs/promises";
import { basename, extname } from "node:path";
import { callRemoteTool } from "./remote-client.js";
import type { RemoteConfig } from "./types.js";

interface CosToken {
  uploadUrl: string;
  cdn: string;
  key: string;
}

interface ObsToken {
  host: string;
  cdn: string;
  accessid: string;
  policy: string;
  signature: string;
  key: string;
}

interface StorageTokenResult {
  storagePlatform: string;
  [REDACTED_SECRET] | ObsToken;
}

function log(...args: unknown[]) {
  console.error("[xiaobai-print]", ...args);
}

function extractText(content: Array<{ type: string; [key: string]: unknown }>): string {
  for (const item of content) {
    if (item.type === "text" && typeof item.text === "string") {
      return item.text;
    }
  }
  return "";
}

export async function uploadLocalFile(
  filePath: string,
  fileName?: string,
  config?: RemoteConfig,
) {
  const effectiveName = fileName ?? basename(filePath);
  const fileData = await readFile(filePath);
  log(`Read local file: ${filePath} (${fileData.byteLength} bytes)`);

  const suffix = extname(effectiveName);
  const tokenArgs = suffix
    ? { fileName: effectiveName, suffix }
    : { fileName: effectiveName };
  const tokenResult = await callRemoteTool("getCosUploadToken", tokenArgs, config);
  const tokenText = extractText(tokenResult.content as Array<{ type: string; [key: string]: unknown }>);
  if (!tokenText) {
    throw new Error("getCosUploadToken returned no text content");
  }

  const storage = JSON.parse(tokenText) as StorageTokenResult;
  const blob = new Blob([fileData]);
  const formData = new FormData();
  let uploadUrl: string;
  let cdnUrl: string;

  if (storage.storagePlatform === "TENCENT_COS") {
    const [REDACTED_SECRET] as CosToken;
    formData.append("file", blob, effectiveName);
    uploadUrl = token.uploadUrl;
    cdnUrl = `${token.cdn}/${token.key}`;
  } else {
    const [REDACTED_SECRET] as ObsToken;
    formData.append("key", token.key);
    formData.append("AccessKeyId", token.accessid);
    formData.append("policy", token.policy);
    formData.append("signature", token.signature);
    formData.append("success_action_status", "200");
    formData.append("file", blob, effectiveName);
    uploadUrl = token.host;
    cdnUrl = `${token.cdn}/${token.key}`;
  }

  log(`Uploading to: ${uploadUrl.substring(0, 80)}...`);
  const response = await fetch(uploadUrl, { method: "POST", body: formData });
  if (!response.ok && response.status !== 204) {
    const body = await response.text().catch(() => "");
    throw new Error(`Upload failed: HTTP ${response.status} ${response.statusText} ${body}`);
  }

  log(`Upload success, CDN URL: ${cdnUrl}`);
  return cdnUrl;
}


path: src/core/remote-client.ts
content: import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { resolveConfig } from "./config.js";
import type { RemoteConfig } from "./types.js";

function log(...args: unknown[]) {
  console.error("[xiaobai-print]", ...args);
}

const clientCache = new Map<string, Promise<Client>>();

function cacheKey(config?: RemoteConfig) {
  const resolved = resolveConfig(config);
  return `${resolved.remoteUrl}::${resolved.token ?? ""}`;
}

export async function getRemoteClient(config?: RemoteConfig) {
  const resolved = resolveConfig(config);
  if (!resolved.token) {
    throw new Error("A remote MCP token is required. Set MY_MCP_TOKEN / OPENCLAW_TOKEN or pass config.token.");
  }

  const key = cacheKey(config);
  const existing = clientCache.get(key);
  if (existing) {
    return existing;
  }

  const clientPromise = (async () => {
    log(`Connecting to remote MCP server: ${resolved.remoteUrl}`);

    const transport = new StreamableHTTPClientTransport(
      new URL(resolved.remoteUrl),
      { requestInit: { headers: { Authorization: `Bearer ${resolved.token}` } } },
    );

    const client = new Client(
      { name: "xiaobai-print-mcp-client", version: "1.0.0" },
      { capabilities: {} },
    );

    await client.connect(transport);
    log("Connected to remote MCP server");
    return client;
  })();

  clientCache.set(key, clientPromise);

  try {
    return await clientPromise;
  } catch (error) {
    clientCache.delete(key);
    throw error;
  }
}

export async function listRemoteTools(config?: RemoteConfig): Promise<Tool[]> {
  const client = await getRemoteClient(config);
  const { tools } = await client.listTools();
  return tools as Tool[];
}

export async function callRemoteTool(
  name: string,
  args: Record<string, unknown> | undefined,
  config?: RemoteConfig,
) {
  const client = await getRemoteClient(config);
  return client.callTool({ name, arguments: args });
}


path: src/core/tool-specs.ts
content: import type { ToolDefinition } from "./types.js";

export const UPLOAD_TOOL: ToolDefinition = {
  name: "uploadFile",
  description:
    "将本地文件上传到云存储（COS/OBS），返回可用于 printCreate 的 CDN 文件地址。",
  inputSchema: {
    type: "object",
    properties: {
      filePath: {
        type: "string",
        description: "本地文件的绝对路径",
      },
      fileName: {
        type: "string",
        description: "文件名（含扩展名），不提供则从路径中提取",
      },
    },
    required: ["filePath"],
    additionalProperties: false,
  },
};

export const PRINT_PROMPT_NAME = "print-instructions";

export const PRINT_INSTRUCTIONS = `# 小白打印助手使用指南

## 打印工作流（必须按顺序执行）

### 第一步：检查设备状态
调用 \`deviceReadyCheck\`，确认设备在线且可打印。
- 如果 \`can_print !== true\`，或 \`box_status\` / \`printer_status\` 异常，停止流程并提示用户检查设备。
- 如果返回 \`printer_info\`，可向用户确认当前设备名称、厂商、型号；排查时优先带上这些信息。
- \`printer_info.mono\` 只作为设备展示信息参考，不能替代 \`deviceGetCapability\` 的能力判断。

### 第二步：获取打印能力
调用 \`deviceGetCapability\`，获取设备支持的纸张尺寸和介质。
- 对照用户需求确认是否支持，不支持时告知用户可用选项。

### 第三步：准备文件
- 网络 URL: 直接使用
- 本地文件: 调用 \`uploadFile\` 上传，获得 CDN 地址

### 第四步：创建打印任务
调用 \`printCreate\`，传入文件地址、文件名、份数等。
- 设备信息自动补全。
- A4 图片（jpg/jpeg）默认按文档模式打印，可理解为图片转文档，这也是大多数用户打印 A4 图片的方式。
- 如果用户明确确认需要 A4 照片纸打印，再使用 \`media_size=A4\` 且 \`media_type=photo\`。

### 第五步：短轮询确认任务已进入执行
调用 \`orderGetStatus\` 查询设计单号。
- 创建任务后先等待 5-10 秒，再查询一次
- 如果返回 \`state\` / \`human_state\` 表示“打印中 / 已接单 / 排队中 / 待打印 / 处理中”等非失败进行态，直接告知用户任务已提交成功，可稍后去打印机取件，然后结束当前流程
- 如果返回明确失败态，再告知失败原因
- 只有在用户明确要求持续跟踪，或需要排查异常时，才继续每隔 10-15 秒查询一次

## 支持的纸张尺寸

| 尺寸 | 介质 | 说明 | 家庭常用 |
|------|------|------|----------|
| A4 | plain | A4 文档 | ✓ |
| A4 | photo | A4 照片 | |
| A3 | plain | A3 文档 | |
| A5 | plain | A5 文档 | |
| B4 | plain | B4 文档 | ✓ |
| B5 | plain | B5 文档 | |
| 5in | photo | 5 寸照片 | |
| 6in | photo | 6 寸照片 | ✓ |
| 7in | photo | 7 寸照片 | |

具体支持哪些尺寸以 \`deviceGetCapability\` 返回结果为准。

## 注意事项

- A4 图片默认推荐 \`A4 + plain/默认\`，按文档模式打印
- A4 照片纸打印仅在用户明确确认后才使用 \`media_type=photo\`
- 照片打印（photo 介质）会忽略单双面、页码范围、奇偶页设置
- 每次打印前都必须执行 \`deviceReadyCheck\`
- \`deviceReadyCheck\` 返回的 \`printer_info\` 适合用于展示或排障；纸张尺寸、介质、彩色、双面能力仍必须以 \`deviceGetCapability\` 为准
- 默认不要长时间占用会话轮询打印结果，确认任务已进入执行即可结束
- 如用户明确要求追踪最终结果，再继续轮询
- 如需取消打印，使用 \`orderCancel\` 传入设计单号
`;


path: src/core/proxy-tools.ts
content: import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { callRemoteTool, listRemoteTools } from "./remote-client.js";
import { UPLOAD_TOOL } from "./tool-specs.js";
import type { RemoteConfig, ToolInvocationResult } from "./types.js";
import { uploadLocalFile } from "./upload-file.js";

function asError(error: unknown): ToolInvocationResult {
  const message = error instanceof Error ? error.message : String(error);
  return {
    content: [{ type: "text", text: `Error: ${message}` }],
    isError: true,
  };
}

export async function listExposedTools(config?: RemoteConfig): Promise<Tool[]> {
  const remoteTools = await listRemoteTools(config);
  return [...remoteTools, UPLOAD_TOOL as Tool];
}

export async function invokeExposedTool(
  name: string,
  args: Record<string, unknown> | undefined,
  config?: RemoteConfig,
): Promise<ToolInvocationResult> {
  try {
    if (name === "uploadFile") {
      const filePath = typeof args?.filePath === "string" ? args.filePath : "";
      if (!filePath) {
        throw new Error("filePath is required");
      }

      const fileName = typeof args?.fileName === "string" ? args.fileName : undefined;
      const cdnUrl = await uploadLocalFile(filePath, fileName, config);
      return {
        content: [{ type: "text", text: cdnUrl }],
      };
    }

    return await callRemoteTool(name, args, config) as ToolInvocationResult;
  } catch (error) {
    return asError(error);
  }
}


path: src/core/types.ts
content: export type JsonSchema = {
  type: string;
  properties?: Record<string, unknown>;
  required?: string[];
  additionalProperties?: boolean;
  items?: unknown;
  description?: string;
};

export interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: JsonSchema;
}

export interface RemoteConfig {
  token?: string;
  remoteUrl?: string;
}

export interface ToolResultText {
  type: "text";
  text: string;
}

export interface ToolInvocationResult {
  content: Array<ToolResultText | Record<string, unknown>>;
  isError?: boolean;
  [key: string]: unknown;
}


path: src/core/config.ts
content: import type { RemoteConfig } from "./types.js";

export const DEFAULT_REMOTE_URL =
  "https://mcp.gongfudou.com/mcp/openclaw/sse";

export function resolveConfig(config?: RemoteConfig) {
  const [REDACTED_SECRET] ??
    process.env.MY_MCP_TOKEN ??
    process.env.OPENCLAW_TOKEN;
  const remoteUrl =
    config?.remoteUrl ??
    process.env.MY_MCP_UPSTREAM_URL ??
    process.env.OPENCLAW_MCP_URL ??
    DEFAULT_REMOTE_URL;

  return { token, remoteUrl };
}


path: skills/xiaobai-print/scripts/invoke.js
content: #!/usr/bin/env node
const DEFAULT_BASE_URL = "http://127.0.0.1:8787";

function normalizeBaseUrl(value) {
  return value.replace(/\/+$/, "");
}

async function main() {
  const [, , toolName, rawArgs] = process.argv;

  if (!toolName) {
    console.error("Missing tool name");
    process.exit(1);
  }

  let args = {};
  if (rawArgs) {
    try {
      args = JSON.parse(rawArgs);
    } catch (error) {
      console.error(`Invalid JSON arguments: ${error instanceof Error ? error.message : String(error)}`);
      process.exit(4);
    }
  }

  if (!args || typeof args !== "object" || Array.isArray(args)) {
    console.error("Arguments must be a JSON object");
    process.exit(4);
  }

  const [REDACTED_SECRET];
  if (!token) {
    console.error("Missing MY_MCP_TOKEN");
    process.exit(2);
  }

  const baseUrl = normalizeBaseUrl(process.env.MY_MCP_BASE_URL || DEFAULT_BASE_URL);
  const response = await fetch(`${baseUrl}/mcp/tools/${encodeURIComponent(toolName)}`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ arguments: args }),
  });

  const text = await response.text();
  if (!response.ok) {
    console.error(text || `Request failed: ${response.status} ${response.statusText}`);
    process.exit(3);
  }

  process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack || error.message : String(error));
  process.exit(10);
});


path: skills/xiaobai-print/schema/tools.json
content: {
  "generatedAt": "2026-03-12T00:00:00.000Z",
  "skillName": "xiaobai-print",
  "description": "小白打印助手。通过本地 MCP bridge 调用打印相关工具，按既定工作流执行设备检查、能力确认、文件上传、任务创建和短状态确认。",
  "bridge": {
    "rawInputUrl": "http://127.0.0.1:8787",
    "baseUrl": "http://127.0.0.1:8787",
    "toolCatalogUrl": "http://127.0.0.1:8787/mcp/tools",
    "toolInvokeBaseUrl": "http://127.0.0.1:8787/mcp/tools"
  },
  "tools": [
    {
      "name": "deviceReadyCheck",
      "description": "检查打印设备是否在线且可用；成功时通常还会返回设备状态和打印机基础信息。",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "description": "通常无需参数；如果 bridge 或上游服务要求额外字段，可按其约定传入。"
      }
    },
    {
      "name": "deviceGetCapability",
      "description": "获取打印设备支持的纸张尺寸、介质、彩色和双面能力。",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "description": "通常无需参数；如果 bridge 或上游服务要求额外字段，可按其约定传入。"
      }
    },
    {
      "name": "printCreate",
      "description": "创建打印任务，参数会透传到 bridge 对接的上游 printCreate。A4 图片（jpg/jpeg）默认按文档模式打印；只有在用户明确确认需要 A4 照片纸时才传 media_type=photo。",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "description": "请传入上游 printCreate 所需的 JSON 字段，常见内容包括文件地址、文件名、打印份数、纸张尺寸、纸张类型、单双面、页码范围、奇偶页、彩色模式等。A4 图片默认推荐 A4 + plain/默认；只有在用户明确确认需要 A4 照片纸时才传 media_type=photo。"
      }
    },
    {
      "name": "getCosUploadToken",
      "description": "获取文件上传凭证，通常由 uploadFile 间接使用。",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "description": "通常无需参数。"
      }
    },
    {
      "name": "uploadFile",
      "description": "上传本地文件并返回可传给 printCreate 的 CDN 地址。",
      "inputSchema": {
        "type": "object",
        "properties": {
          "filePath": {
            "type": "string",
            "description": "本地文件的绝对路径"
          },
          "fileName": {
            "type": "string",
            "description": "文件名（含扩展名），不提供则从路径中提取"
          }
        },
        "required": [
          "filePath"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "orderGetStatus",
      "description": "查询打印任务状态。",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "description": "请传入 bridge 或上游状态查询接口要求的 JSON 字段。"
      }
    },
    {
      "name": "orderCancel",
      "description": "取消打印任务。",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": true,
        "description": "请传入 bridge 或上游取消接口要求的 JSON 字段。"
      }
    }
  ]
}


path: examples/search-docs-skill/scripts/invoke.js
content: #!/usr/bin/env node
const DEFAULT_BASE_URL = "http://localhost:8787";

function normalizeBaseUrl(value) {
  return value.replace(/\/+$/, "");
}

async function main() {
  const [, , toolName, rawArgs] = process.argv;

  if (!toolName) {
    console.error("Missing tool name");
    process.exit(1);
  }

  let args = {};
  if (rawArgs) {
    try {
      args = JSON.parse(rawArgs);
    } catch (error) {
      console.error(`Invalid JSON arguments: ${error instanceof Error ? error.message : String(error)}`);
      process.exit(4);
    }
  }

  if (!args || typeof args !== "object" || Array.isArray(args)) {
    console.error("Arguments must be a JSON object");
    process.exit(4);
  }

  const [REDACTED_SECRET];
  if (!token) {
    console.error("Missing MY_MCP_TOKEN");
    process.exit(2);
  }

  const baseUrl = normalizeBaseUrl(process.env.MY_MCP_BASE_URL || DEFAULT_BASE_URL || "http://127.0.0.1:8787");
  const response = await fetch(`${baseUrl}/mcp/tools/${encodeURIComponent(toolName)}`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ arguments: args }),
  });

  const text = await response.text();
  if (!response.ok) {
    console.error(text || `Request failed: ${response.status} ${response.statusText}`);
    process.exit(3);
  }

  process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack || error.message : String(error));
  process.exit(10);
});


path: examples/search-docs-skill/schema/tools.json
content: {
  "generatedAt": "2026-03-12T00:00:00.000Z",
  "skillName": "search-docs-skill",
  "description": "Use this skill when the user needs capabilities exposed by the local MCP bridge. It wraps 1 MCP tool behind a local Node.js wrapper.",
  "bridge": {
    "rawInputUrl": "http://localhost:8787",
    "baseUrl": "http://localhost:8787",
    "toolCatalogUrl": "http://localhost:8787/mcp/tools",
    "toolInvokeBaseUrl": "http://localhost:8787/mcp/tools"
  },
  "tools": [
    {
      "name": "search_docs",
      "description": "Search documents by keyword",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string"
          },
          "limit": {
            "type": "number"
          }
        },
        "required": [
          "query"
        ]
      }
    }
  ]
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)