Skip to content

Commit de17772

Browse files
committed
feat: 🤖 add IronClaw trusted MCP support
1 parent 3e2cde7 commit de17772

File tree

12 files changed

+780
-8
lines changed

12 files changed

+780
-8
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Unit Tests](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml/badge.svg)](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml)
66
[![Coverage](https://img.shields.io/endpoint?url=https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json)](https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json)
77

8-
AelfScan explorer skill toolkit for AI agents, with **SDK + MCP + CLI + OpenClaw** interfaces.
8+
AelfScan explorer skill toolkit for AI agents, with **SDK + MCP + CLI + OpenClaw + IronClaw** interfaces.
99

1010
## Features
1111

@@ -86,11 +86,33 @@ Use [`mcp-config.example.json`](./mcp-config.example.json).
8686
bun run setup claude
8787
bun run setup cursor
8888
bun run setup cursor --global
89+
bun run setup ironclaw
8990
bun run setup openclaw
9091
bun run setup list
9192
bun run build:openclaw
9293
```
9394

95+
## IronClaw
96+
97+
```bash
98+
bun run setup ironclaw
99+
bun run setup uninstall ironclaw
100+
```
101+
102+
The IronClaw setup writes a stdio MCP entry to `~/.ironclaw/mcp-servers.json` and installs this repo's `SKILL.md` to `~/.ironclaw/skills/aelfscan-skill/SKILL.md`.
103+
104+
Important trust model note:
105+
106+
- Use the trusted skill path above even for this read-only package so routing stays stable.
107+
- Do not rely on `~/.ironclaw/installed_skills/` for the primary install path.
108+
- This MCP server emits both standard MCP camelCase annotations and IronClaw-compatible snake_case annotations so the current IronClaw source can honor read-only hints.
109+
110+
Minimal smoke test:
111+
112+
1. `bun run setup ironclaw`
113+
2. Ask IronClaw to search AelfScan explorer data, such as `latest ELF transactions`
114+
3. Confirm the skill routes for analytics/search prompts and stays read-only
115+
94116
## Tests
95117

96118
```bash

README.zh-CN.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Unit Tests](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml/badge.svg)](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml)
66
[![Coverage](https://img.shields.io/endpoint?url=https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json)](https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json)
77

8-
面向 AI Agent 的 AelfScan 浏览器能力工具包,提供 **SDK + MCP + CLI + OpenClaw** 四种使用方式
8+
面向 AI Agent 的 AelfScan 浏览器能力工具包,提供 **SDK + MCP + CLI + OpenClaw + IronClaw** 五种使用方式
99

1010
## 功能覆盖
1111

@@ -86,11 +86,33 @@ bun run aelfscan_skill.ts statistics metric --input '{"metric":"dailyTransaction
8686
bun run setup claude
8787
bun run setup cursor
8888
bun run setup cursor --global
89+
bun run setup ironclaw
8990
bun run setup openclaw
9091
bun run setup list
9192
bun run build:openclaw
9293
```
9394

95+
## IronClaw
96+
97+
```bash
98+
bun run setup ironclaw
99+
bun run setup uninstall ironclaw
100+
```
101+
102+
IronClaw 安装会向 `~/.ironclaw/mcp-servers.json` 写入 stdio MCP entry,并把当前仓库的 `SKILL.md` 安装到 `~/.ironclaw/skills/aelfscan-skill/SKILL.md`
103+
104+
关于 trust model 的说明:
105+
106+
- 即使这是只读 skill,也建议走上面的 trusted skill 路径,保证路由稳定。
107+
- 不要把 `~/.ironclaw/installed_skills/` 当成主安装路径。
108+
- 当前 MCP server 会同时输出标准 MCP camelCase annotations 和 IronClaw 兼容 snake_case annotations,确保 IronClaw 能识别 read-only hint。
109+
110+
最短 smoke test:
111+
112+
1. `bun run setup ironclaw`
113+
2. 让 IronClaw 查询 AelfScan 浏览器数据,例如 `latest ELF transactions`
114+
3. 确认 analytics/search prompt 会命中该 skill,且整个能力保持只读
115+
94116
## 测试
95117

96118
```bash

SKILL.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,54 @@
11
---
22
name: "aelfscan-skill"
3+
version: "0.2.2"
34
description: "AelfScan explorer data retrieval and analytics skill for agents."
5+
activation:
6+
keywords:
7+
- explorer
8+
- analytics
9+
- search
10+
- address
11+
- token
12+
- nft
13+
- statistics
14+
- block
15+
- transaction
16+
exclude_keywords:
17+
- transfer
18+
- swap
19+
- wallet
20+
- guardian
21+
- proposal
22+
tags:
23+
- explorer
24+
- analytics
25+
- aelf
26+
- aelfscan
27+
max_context_tokens: 1600
428
---
529

630
# AelfScan Skill
731

832
## When to use
933
- Use this skill when you need AelfScan explorer search and analytics data retrieval tasks.
34+
- Default to this skill for explorer, search, statistics, and historical analysis requests on aelf.
1035

1136
## Capabilities
1237
- Domain coverage: search, blockchain, address, token, NFT, statistics
1338
- Single tool descriptor source for SDK/CLI/MCP/OpenClaw
39+
- Supports SDK, CLI, MCP, OpenClaw, and IronClaw integration from one codebase.
1440
- MCP output governance controls and standardized trace-aware errors
15-
- Supports SDK, CLI, MCP, and OpenClaw integration from one codebase.
1641

1742
## Safe usage rules
1843
- Never print private keys, mnemonics, or tokens in channel outputs.
1944
- This skill is read-only; do not attempt to execute chain writes via this package.
2045
- If user intent requires writes, route to wallet + domain write skills and keep this skill for analytics.
46+
- Do not use this skill for transfer, swap, wallet management, guardian, or governance write workflows.
2147

2248
## Command recipes
2349
- Start MCP server: `bun run mcp`
2450
- Run CLI entry: `bun run cli`
51+
- Install into IronClaw: `bun run setup ironclaw`
2552
- Generate OpenClaw config: `bun run build:openclaw`
2653
- Verify OpenClaw config: `bun run build:openclaw:check`
2754
- Run CI coverage gate: `bun run test:coverage:ci`

bin/platforms/ironclaw.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import * as fs from 'node:fs';
2+
import * as os from 'node:os';
3+
import * as path from 'node:path';
4+
import {
5+
SERVER_NAME,
6+
getBunPath,
7+
getMcpServerPath,
8+
getPackageRoot,
9+
readJsonFile,
10+
writeJsonFile,
11+
} from './utils.js';
12+
13+
export const IRONCLAW_CONFIG_SCHEMA_VERSION = 1;
14+
15+
export type IronclawStdioTransport = {
16+
transport: 'stdio';
17+
command: string;
18+
args: string[];
19+
env: Record<string, string>;
20+
};
21+
22+
export type IronclawMcpServer = {
23+
name: string;
24+
url: string;
25+
transport: IronclawStdioTransport;
26+
headers: Record<string, string>;
27+
enabled: boolean;
28+
description?: string;
29+
};
30+
31+
export type IronclawMcpConfig = {
32+
schema_version: number;
33+
servers: IronclawMcpServer[];
34+
};
35+
36+
type MergeAction = 'created' | 'updated' | 'skipped';
37+
type SkillAction = 'created' | 'updated' | 'skipped';
38+
39+
function getIronclawBaseDir(): string {
40+
return path.join(os.homedir(), '.ironclaw');
41+
}
42+
43+
export function getIronclawMcpConfigPath(): string {
44+
return path.join(getIronclawBaseDir(), 'mcp-servers.json');
45+
}
46+
47+
export function getIronclawSkillsDir(): string {
48+
return path.join(getIronclawBaseDir(), 'skills');
49+
}
50+
51+
export function getIronclawSkillInstallPath(skillsDir = getIronclawSkillsDir()): string {
52+
return path.join(skillsDir, SERVER_NAME, 'SKILL.md');
53+
}
54+
55+
function getBundledSkillPath(): string {
56+
return path.join(getPackageRoot(), 'SKILL.md');
57+
}
58+
59+
function normalizeIronclawMcpConfig(existing: any): IronclawMcpConfig {
60+
return {
61+
schema_version:
62+
typeof existing?.schema_version === 'number'
63+
? existing.schema_version
64+
: IRONCLAW_CONFIG_SCHEMA_VERSION,
65+
servers: Array.isArray(existing?.servers) ? [...existing.servers] : [],
66+
};
67+
}
68+
69+
export function generateIronclawServerEntry(customServerPath?: string): IronclawMcpServer {
70+
return {
71+
name: SERVER_NAME,
72+
url: '',
73+
transport: {
74+
transport: 'stdio',
75+
command: getBunPath(),
76+
args: ['run', customServerPath || getMcpServerPath()],
77+
env: {
78+
AELFSCAN_API_BASE_URL: 'https://aelfscan.io',
79+
},
80+
},
81+
headers: {},
82+
enabled: true,
83+
description: 'AelfScan explorer MCP server for IronClaw',
84+
};
85+
}
86+
87+
export function mergeIronclawMcpConfig(
88+
existing: any,
89+
entry: IronclawMcpServer,
90+
force = false,
91+
): { config: IronclawMcpConfig; action: MergeAction } {
92+
const config = normalizeIronclawMcpConfig(existing);
93+
const index = config.servers.findIndex(server => server?.name === entry.name);
94+
95+
if (index >= 0 && !force) {
96+
return { config, action: 'skipped' };
97+
}
98+
99+
if (index >= 0) {
100+
const servers = [...config.servers];
101+
servers[index] = entry;
102+
return {
103+
config: { ...config, schema_version: IRONCLAW_CONFIG_SCHEMA_VERSION, servers },
104+
action: 'updated',
105+
};
106+
}
107+
108+
return {
109+
config: {
110+
...config,
111+
schema_version: IRONCLAW_CONFIG_SCHEMA_VERSION,
112+
servers: [...config.servers, entry],
113+
},
114+
action: 'created',
115+
};
116+
}
117+
118+
export function removeIronclawMcpConfig(
119+
existing: any,
120+
serverName = SERVER_NAME,
121+
): { config: IronclawMcpConfig; removed: boolean } {
122+
const config = normalizeIronclawMcpConfig(existing);
123+
const servers = config.servers.filter(server => server?.name !== serverName);
124+
125+
return {
126+
config: {
127+
...config,
128+
schema_version: IRONCLAW_CONFIG_SCHEMA_VERSION,
129+
servers,
130+
},
131+
removed: servers.length !== config.servers.length,
132+
};
133+
}
134+
135+
function installIronclawSkill(
136+
skillsDir = getIronclawSkillsDir(),
137+
force = false,
138+
): { action: SkillAction; skillPath: string } {
139+
const sourcePath = getBundledSkillPath();
140+
const targetPath = getIronclawSkillInstallPath(skillsDir);
141+
const targetDir = path.dirname(targetPath);
142+
const existedBefore = fs.existsSync(targetPath);
143+
144+
if (!fs.existsSync(sourcePath)) {
145+
throw new Error(`Bundled SKILL.md not found at ${sourcePath}`);
146+
}
147+
148+
if (existedBefore && !force) {
149+
return { action: 'skipped', skillPath: targetPath };
150+
}
151+
152+
fs.mkdirSync(targetDir, { recursive: true });
153+
fs.copyFileSync(sourcePath, targetPath);
154+
155+
return {
156+
action: existedBefore ? 'updated' : 'created',
157+
skillPath: targetPath,
158+
};
159+
}
160+
161+
export function assertSafeIronclawSkillDir(skillsDir: string, skillDir: string): void {
162+
const resolvedSkillsDir = path.resolve(skillsDir);
163+
const resolvedSkillDir = path.resolve(skillDir);
164+
const expectedSkillDir = path.dirname(getIronclawSkillInstallPath(resolvedSkillsDir));
165+
const relative = path.relative(resolvedSkillsDir, resolvedSkillDir);
166+
167+
if (
168+
resolvedSkillDir !== expectedSkillDir ||
169+
path.isAbsolute(relative) ||
170+
relative.startsWith('..') ||
171+
path.basename(resolvedSkillDir) !== SERVER_NAME
172+
) {
173+
throw new Error(`Refusing to remove unexpected IronClaw skill path: ${resolvedSkillDir}`);
174+
}
175+
}
176+
177+
function removeIronclawSkill(
178+
skillsDir = getIronclawSkillsDir(),
179+
): { skillRemoved: boolean; skillPath: string } {
180+
const skillPath = getIronclawSkillInstallPath(skillsDir);
181+
const skillDir = path.dirname(skillPath);
182+
const exists = fs.existsSync(skillDir);
183+
184+
if (exists) {
185+
assertSafeIronclawSkillDir(skillsDir, skillDir);
186+
fs.rmSync(skillDir, { recursive: true, force: true });
187+
}
188+
189+
return { skillRemoved: exists, skillPath };
190+
}
191+
192+
export function setupIronclaw(opts: {
193+
mcpConfigPath?: string;
194+
skillsDir?: string;
195+
serverPath?: string;
196+
force?: boolean;
197+
}) {
198+
const mcpConfigPath = opts.mcpConfigPath || getIronclawMcpConfigPath();
199+
const existing = readJsonFile(mcpConfigPath);
200+
const entry = generateIronclawServerEntry(opts.serverPath);
201+
const { config, action } = mergeIronclawMcpConfig(existing, entry, opts.force);
202+
const skill = installIronclawSkill(opts.skillsDir, opts.force);
203+
204+
if (action !== 'skipped') {
205+
writeJsonFile(mcpConfigPath, config);
206+
}
207+
208+
console.log(`[${action.toUpperCase()}] IronClaw MCP config → ${mcpConfigPath}`);
209+
console.log(`[${skill.action.toUpperCase()}] Trusted skill → ${skill.skillPath}`);
210+
211+
return {
212+
mcpAction: action,
213+
skillAction: skill.action,
214+
mcpConfigPath,
215+
skillPath: skill.skillPath,
216+
};
217+
}
218+
219+
export function uninstallIronclaw(opts: {
220+
mcpConfigPath?: string;
221+
skillsDir?: string;
222+
}) {
223+
const mcpConfigPath = opts.mcpConfigPath || getIronclawMcpConfigPath();
224+
const existing = readJsonFile(mcpConfigPath);
225+
const { config, removed } = removeIronclawMcpConfig(existing);
226+
const skill = removeIronclawSkill(opts.skillsDir);
227+
228+
if (removed) {
229+
writeJsonFile(mcpConfigPath, config);
230+
}
231+
232+
if (!removed) {
233+
console.log(`[SKIP] "${SERVER_NAME}" not found in ${mcpConfigPath}`);
234+
} else {
235+
console.log(`[REMOVED] "${SERVER_NAME}" from ${mcpConfigPath}`);
236+
}
237+
238+
if (!skill.skillRemoved) {
239+
console.log(`[SKIP] Trusted skill not found at ${skill.skillPath}`);
240+
} else {
241+
console.log(`[REMOVED] Trusted skill → ${skill.skillPath}`);
242+
}
243+
244+
return {
245+
mcpRemoved: removed,
246+
skillRemoved: skill.skillRemoved,
247+
mcpConfigPath,
248+
skillPath: skill.skillPath,
249+
};
250+
}

0 commit comments

Comments
 (0)