
Aelfscan Skill
- 1 installs
- 5 repo stars
- Updated March 9, 2026
- aelfscanproject/aelfscan-skill
aelfscan-skill is a Claude Code skill that retrieves read-only AelfScan blockchain-explorer search and analytics data for agents.
About
aelfscan-skill is a Claude Code skill for retrieving AelfScan blockchain-explorer search and analytics data. A developer uses it to query the aelf chain for addresses, tokens, NFTs, and statistics. It is read-only and exposes a single tool descriptor source that drives SDK, CLI, MCP, and OpenClaw integrations, with rules against printing private keys or performing chain writes.
- Read-only AelfScan explorer data retrieval and analytics for agents
- Covers search, blockchain, address, token, NFT, and statistics domains
- One codebase feeds SDK, CLI, MCP, and OpenClaw integrations
Aelfscan Skill by the numbers
- 1 all-time installs (skills.sh)
- Ranked #426 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Jul 7, 2026 (Skillselion catalog sync)
aelfscan-skill capabilities & compatibility
- Capabilities
- explorer search · token lookup · nft lookup · chain statistics
- Use cases
- data analysis · research
- Pricing
- Free
What aelfscan-skill says it does
AelfScan explorer data retrieval and analytics skill for agents.
Domain coverage: search, blockchain, address, token, NFT, statistics
This skill is read-only; do not attempt to execute chain writes via this package.
npx skills add https://github.com/aelfscanproject/aelfscan-skill --skill aelfscan-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 5 |
| Last updated | March 9, 2026 |
| Repository | aelfscanproject/aelfscan-skill ↗ |
What it does
Retrieve AelfScan explorer search and analytics data for addresses, tokens, NFTs, and chain statistics.
Who is it for?
Querying the aelf blockchain explorer for address, token, NFT, and statistics data from an agent.
Skip if: Signing transactions, chain writes, or wallet custody.
When should I use this skill?
You need AelfScan explorer search and analytics data retrieval.
What you get
Agents retrieve AelfScan address, token, NFT, and statistics data through one read-only descriptor source.
- AelfScan explorer query results
- MCP/CLI/SDK access to search, token, NFT, and statistics data
By the numbers
- 6 domain areas: search, blockchain, address, token, NFT, statistics
- 4 integration surfaces: SDK, CLI, MCP, OpenClaw
Files
AelfScan Skill
When to use
- Use this skill when you need AelfScan explorer search and analytics data retrieval tasks.
Capabilities
- Domain coverage: search, blockchain, address, token, NFT, statistics
- Single tool descriptor source for SDK/CLI/MCP/OpenClaw
- MCP output governance controls and standardized trace-aware errors
- Supports SDK, CLI, MCP, and OpenClaw integration from one codebase.
Safe usage rules
- Never print private keys, mnemonics, or tokens in channel outputs.
- This skill is read-only; do not attempt to execute chain writes via this package.
- If user intent requires writes, route to wallet + domain write skills and keep this skill for analytics.
Command recipes
- Start MCP server:
bun run mcp - Run CLI entry:
bun run cli - Generate OpenClaw config:
bun run build:openclaw - Verify OpenClaw config:
bun run build:openclaw:check - Run CI coverage gate:
bun run test:coverage:ci
Limits / Non-goals
- This skill focuses on domain operations and adapters; it is not a full wallet custody system.
- It does not consume signer context for transaction signing.
- Do not hardcode environment secrets in source code or docs.
- Avoid bypassing validation for external service calls.
# Base URL of aelfscan API
AELFSCAN_API_BASE_URL=https://aelfscan.io
# Default chain id for requests when chainId is omitted.
# Use empty string for multi-chain scope.
AELFSCAN_DEFAULT_CHAIN_ID=
# HTTP timeout in milliseconds
AELFSCAN_TIMEOUT_MS=10000
# Retry count for transient request failures
AELFSCAN_RETRY=1
# Retry backoff base milliseconds and max milliseconds (exponential + jitter)
AELFSCAN_RETRY_BASE_MS=200
AELFSCAN_RETRY_MAX_MS=3000
# HTTP client concurrency limit
AELFSCAN_MAX_CONCURRENT_REQUESTS=5
# Default in-memory cache TTL for statistics GET requests (milliseconds)
AELFSCAN_CACHE_TTL_MS=60000
# Maximum in-memory cache entries before FIFO eviction
AELFSCAN_CACHE_MAX_ENTRIES=500
# Pagination maxResultCount upper bound
AELFSCAN_MAX_RESULT_COUNT=200
# MCP output controls
AELFSCAN_MCP_MAX_ITEMS=50
AELFSCAN_MCP_MAX_CHARS=60000
AELFSCAN_MCP_INCLUDE_RAW=false
name: Coverage Badge
on:
push:
branches:
- main
- master
- 'codex/**'
workflow_dispatch:
permissions:
contents: write
jobs:
coverage-badge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run test:coverage:ci
- run: bun run coverage:badge
- run: touch coverage/.nojekyll
- name: Deploy coverage badge to gh-pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./coverage
publish_branch: gh-pages
name: Publish to npm
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Type check
run: bunx tsc --noEmit
- name: Coverage gate
run: bun run test:coverage:ci
- name: Verify generated openclaw config
run: bun run build:openclaw:check
publish:
if: startsWith(github.ref, 'refs/tags/v')
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Ensure latest npm (trusted publishing requires >= 11.5.1)
run: npm install -g npm@latest
- name: Verify tag matches package.json version
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
TAG_VERSION="${GITHUB_REF_NAME#v}"
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match package.json ($PKG_VERSION)"
exit 1
fi
- run: npm publish --provenance --access public
name: Test
on:
pull_request:
push:
branches:
- main
- master
- 'codex/**'
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Check dependency baseline
run: bun run deps:check
- name: Type check
run: bunx tsc --noEmit
- name: Coverage gate
run: bun run test:coverage:ci
- name: Verify generated openclaw config
run: bun run build:openclaw:check
- name: Upload coverage to Codecov
if: ${{ env.CODECOV_TOKEN != '' }}
uses: codecov/codecov-action@v5
with:
token: ${{ env.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_ci_if_error: true
- name: Skip Codecov upload (missing token)
if: ${{ env.CODECOV_TOKEN == '' }}
run: echo "CODECOV_TOKEN is not set; skipping Codecov upload."
node_modules
coverage
*.log
.DS_Store
.env
#!/usr/bin/env bun
import { Command } from 'commander';
import { ZodError } from 'zod';
import { CLI_TOOL_DESCRIPTOR_BY_KEY } from './src/tooling/tool-descriptors.js';
function parseInput(raw?: string): Record<string, unknown> {
if (!raw) {
return {};
}
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Input must be a JSON object.');
}
return parsed as Record<string, unknown>;
} catch (error) {
throw new Error(`Invalid --input JSON: ${(error as Error).message}`);
}
}
async function runCommand(domain: string, action: string, inputRaw?: string): Promise<void> {
const key = `${domain}.${action}`;
const descriptor = CLI_TOOL_DESCRIPTOR_BY_KEY.get(key);
if (!descriptor) {
throw new Error(`Unsupported command: ${key}`);
}
const input = parseInput(inputRaw);
const validatedInput = descriptor.parse(input);
const result = await descriptor.handler(validatedInput);
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (!result.success) {
process.exitCode = 1;
}
}
const program = new Command();
program
.name('aelfscan-skill')
.description('AelfScan skill CLI (search/blockchain/address/token/nft/statistics)')
.argument('<domain>', 'search | blockchain | address | token | nft | statistics')
.argument('<action>', 'action name under domain')
.option('--input <json>', 'JSON input payload')
.action(async (domain, action, options: { input?: string }) => {
await runCommand(domain, action, options.input);
});
program.parseAsync(process.argv).catch((error: unknown) => {
if (error instanceof ZodError) {
process.stderr.write(`[ERROR] Invalid input: ${error.message}\n`);
process.exit(1);
return;
}
process.stderr.write(`[ERROR] ${(error as Error).message}\n`);
process.exit(1);
});
#!/usr/bin/env bun
import * as fs from 'node:fs';
import * as path from 'node:path';
const root = path.resolve(import.meta.dir, '..');
const lcovPath = path.join(root, 'coverage', 'lcov.info');
const outputPath = path.join(root, 'coverage', 'coverage-badge.json');
if (!fs.existsSync(lcovPath)) {
process.stderr.write(`[ERROR] lcov not found: ${lcovPath}\n`);
process.exit(1);
}
const lcov = fs.readFileSync(lcovPath, 'utf-8');
let linesFound = 0;
let linesHit = 0;
for (const line of lcov.split('\n')) {
if (line.startsWith('LF:')) {
linesFound += Number(line.slice(3)) || 0;
}
if (line.startsWith('LH:')) {
linesHit += Number(line.slice(3)) || 0;
}
}
const coverage = linesFound > 0 ? (linesHit / linesFound) * 100 : 0;
const rounded = Math.round(coverage * 100) / 100;
let color = 'red';
if (rounded >= 90) {
color = 'brightgreen';
} else if (rounded >= 80) {
color = 'green';
} else if (rounded >= 70) {
color = 'yellowgreen';
} else if (rounded >= 60) {
color = 'yellow';
} else if (rounded >= 50) {
color = 'orange';
}
const badge = {
schemaVersion: 1,
label: 'coverage',
message: `${rounded}%`,
color,
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(badge, null, 2)}\n`, 'utf-8');
process.stdout.write(`[OK] Coverage badge generated: ${outputPath} (${badge.message})\n`);
#!/usr/bin/env bun
import * as fs from 'node:fs';
import * as path from 'node:path';
import { OPENCLAW_TOOL_DESCRIPTORS } from '../src/tooling/tool-descriptors.js';
const packageRoot = path.resolve(import.meta.dir, '..');
const targetPath = path.join(packageRoot, 'openclaw.json');
const openclaw = {
name: 'aelfscan-skill',
description: 'AelfScan explorer tools for search, blockchain, addresses, tokens, NFTs, and statistics.',
tools: OPENCLAW_TOOL_DESCRIPTORS.map(descriptor => ({
name: descriptor.mcpName,
description: descriptor.description,
command: 'bun',
args: ['run', 'aelfscan_skill.ts', descriptor.domain, descriptor.action],
cwd: '.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: true,
},
})),
};
const serialized = `${JSON.stringify(openclaw, null, 2)}\n`;
const checkMode = process.argv.includes('--check');
if (checkMode) {
if (!fs.existsSync(targetPath)) {
process.stderr.write(`[ERROR] ${targetPath} does not exist\n`);
process.exit(1);
}
const existing = fs.readFileSync(targetPath, 'utf-8');
if (existing !== serialized) {
process.stderr.write('[ERROR] openclaw.json is out of date. Run `bun run build:openclaw`\n');
process.exit(1);
}
process.stdout.write('[OK] openclaw.json is up to date\n');
process.exit(0);
}
fs.writeFileSync(targetPath, serialized, 'utf-8');
process.stdout.write(`[OK] Generated ${targetPath} with ${openclaw.tools.length} tools\n`);
import {
LOG,
SERVER_NAME,
generateMcpEntry,
getPlatformPaths,
mergeMcpConfig,
readJsonFile,
removeMcpEntry,
writeJsonFile,
} from './utils.js';
export function setupClaude(opts: { configPath?: string; serverPath?: string; force?: boolean }): boolean {
const configPath = opts.configPath || getPlatformPaths().claude;
const entry = generateMcpEntry(opts.serverPath);
LOG.step(`Config file: ${configPath}`);
LOG.step(`MCP server: ${entry.args[1]}`);
const existing = readJsonFile(configPath);
const { config, action } = mergeMcpConfig(existing, SERVER_NAME, entry, opts.force);
if (action === 'skipped') {
LOG.warn(`\"${SERVER_NAME}\" already exists in Claude Desktop config.`);
LOG.info('Use --force to overwrite.');
return false;
}
writeJsonFile(configPath, config);
LOG.success(`Claude Desktop MCP config ${action}: ${configPath}`);
LOG.info('Restart Claude Desktop to apply changes.');
return true;
}
export function uninstallClaude(opts: { configPath?: string }): boolean {
const configPath = opts.configPath || getPlatformPaths().claude;
const existing = readJsonFile(configPath);
const { config, removed } = removeMcpEntry(existing, SERVER_NAME);
if (!removed) {
LOG.info(`\"${SERVER_NAME}\" not found in Claude Desktop config.`);
return false;
}
writeJsonFile(configPath, config);
LOG.success(`Removed \"${SERVER_NAME}\" from Claude Desktop config.`);
return true;
}
import {
LOG,
SERVER_NAME,
generateMcpEntry,
getCursorProjectPath,
getPlatformPaths,
mergeMcpConfig,
readJsonFile,
removeMcpEntry,
writeJsonFile,
} from './utils.js';
export function setupCursor(opts: {
global?: boolean;
configPath?: string;
serverPath?: string;
force?: boolean;
projectDir?: string;
}): boolean {
let configPath: string;
let scope: 'global' | 'project' | 'custom';
if (opts.configPath) {
configPath = opts.configPath;
scope = 'custom';
} else if (opts.global) {
configPath = getPlatformPaths().cursorGlobal;
scope = 'global';
} else {
configPath = getCursorProjectPath(opts.projectDir);
scope = 'project';
}
const entry = generateMcpEntry(opts.serverPath);
LOG.step(`Scope: ${scope}`);
LOG.step(`Config file: ${configPath}`);
LOG.step(`MCP server: ${entry.args[1]}`);
const existing = readJsonFile(configPath);
const { config, action } = mergeMcpConfig(existing, SERVER_NAME, entry, opts.force);
if (action === 'skipped') {
LOG.warn(`\"${SERVER_NAME}\" already exists in Cursor ${scope} config.`);
LOG.info('Use --force to overwrite.');
return false;
}
writeJsonFile(configPath, config);
LOG.success(`Cursor ${scope} MCP config ${action}: ${configPath}`);
return true;
}
export function uninstallCursor(opts: { global?: boolean; configPath?: string; projectDir?: string }): boolean {
let configPath: string;
if (opts.configPath) {
configPath = opts.configPath;
} else if (opts.global) {
configPath = getPlatformPaths().cursorGlobal;
} else {
configPath = getCursorProjectPath(opts.projectDir);
}
const existing = readJsonFile(configPath);
const { config, removed } = removeMcpEntry(existing, SERVER_NAME);
if (!removed) {
LOG.info(`\"${SERVER_NAME}\" not found in Cursor config: ${configPath}`);
return false;
}
writeJsonFile(configPath, config);
LOG.success(`Removed \"${SERVER_NAME}\" from Cursor config: ${configPath}`);
return true;
}
import * as fs from 'node:fs';
import * as path from 'node:path';
import { LOG, getPackageRoot, readJsonFile, writeJsonFile } from './utils.js';
interface OpenClawTool {
name: string;
description?: string;
command?: string;
args?: string[];
cwd?: string;
[key: string]: unknown;
}
function getSourceFilePath(): string {
return path.join(getPackageRoot(), 'openclaw.json');
}
function loadSourceTools(): OpenClawTool[] {
const sourcePath = getSourceFilePath();
if (!fs.existsSync(sourcePath)) {
throw new Error(`openclaw.json not found at ${sourcePath}`);
}
const source = readJsonFile(sourcePath);
const tools = Array.isArray(source.tools) ? source.tools : [];
if (!tools.length) {
throw new Error('No tools found in openclaw.json');
}
return tools;
}
export function setupOpenClaw(opts: { configPath?: string; cwd?: string; force?: boolean }): boolean {
let tools: OpenClawTool[];
try {
tools = loadSourceTools();
} catch (error) {
LOG.error((error as Error).message);
return false;
}
const resolvedCwd = opts.cwd || getPackageRoot();
const normalizedTools = tools.map(tool => ({
...tool,
cwd: resolvedCwd,
}));
if (!opts.configPath) {
const outputPath = path.join(process.cwd(), 'aelfscan-openclaw.json');
writeJsonFile(outputPath, { tools: normalizedTools });
LOG.success(`Generated standalone OpenClaw config: ${outputPath}`);
LOG.info(`Tools count: ${normalizedTools.length}; cwd=${resolvedCwd}`);
return true;
}
LOG.step(`Merging ${normalizedTools.length} tools into: ${opts.configPath}`);
const existing = readJsonFile(opts.configPath);
if (!Array.isArray(existing.tools)) {
existing.tools = [];
}
let added = 0;
let updated = 0;
for (const tool of normalizedTools) {
const index = existing.tools.findIndex((item: OpenClawTool) => item.name === tool.name);
if (index >= 0) {
if (opts.force) {
existing.tools[index] = tool;
updated += 1;
}
continue;
}
existing.tools.push(tool);
added += 1;
}
writeJsonFile(opts.configPath, existing);
LOG.success(`OpenClaw config updated: ${added} added, ${updated} updated.`);
return true;
}
export function uninstallOpenClaw(opts: { configPath?: string }): boolean {
if (!opts.configPath) {
LOG.info('Provide --config-path to remove tools from an existing OpenClaw config.');
return false;
}
let toolNames: Set<string>;
try {
toolNames = new Set(loadSourceTools().map(tool => tool.name));
} catch (error) {
LOG.error((error as Error).message);
return false;
}
const existing = readJsonFile(opts.configPath);
if (!Array.isArray(existing.tools)) {
LOG.info('No tools found in target OpenClaw config.');
return false;
}
const before = existing.tools.length;
existing.tools = existing.tools.filter((tool: OpenClawTool) => !toolNames.has(tool.name));
const removed = before - existing.tools.length;
if (removed <= 0) {
LOG.info('No aelfscan-skill tools found in target OpenClaw config.');
return false;
}
writeJsonFile(opts.configPath, existing);
LOG.success(`Removed ${removed} aelfscan-skill tools from OpenClaw config.`);
return true;
}
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
export const SERVER_NAME = 'aelfscan-skill';
export interface PlatformPaths {
claude: string;
cursorGlobal: string;
}
export interface McpServerEntry {
command: string;
args: string[];
env: Record<string, string>;
}
export function getPackageRoot(): string {
return path.resolve(import.meta.dir, '..', '..');
}
export function getMcpServerPath(): string {
return path.join(getPackageRoot(), 'src', 'mcp', 'server.ts');
}
export function getBunPath(): string {
try {
const cmd = os.platform() === 'win32' ? ['where', 'bun'] : ['which', 'bun'];
const result = Bun.spawnSync(cmd);
const output = result.stdout.toString().trim();
if (output) {
return output.split('\n')[0].trim();
}
} catch {
// ignore and use fallback
}
return 'bun';
}
export function getPlatformPaths(): PlatformPaths {
const home = os.homedir();
const platform = os.platform();
let claude: string;
if (platform === 'darwin') {
claude = path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
} else if (platform === 'win32') {
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
claude = path.join(appData, 'Claude', 'claude_desktop_config.json');
} else {
claude = path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
}
return {
claude,
cursorGlobal: path.join(home, '.cursor', 'mcp.json'),
};
}
export function getCursorProjectPath(projectDir?: string): string {
const baseDir = projectDir || process.cwd();
return path.join(baseDir, '.cursor', 'mcp.json');
}
export function readJsonFile(filePath: string): any {
try {
if (!fs.existsSync(filePath)) {
return {};
}
const text = fs.readFileSync(filePath, 'utf8');
return JSON.parse(text);
} catch {
return {};
}
}
export function writeJsonFile(filePath: string, data: any): void {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
}
export function generateMcpEntry(customServerPath?: string): McpServerEntry {
return {
command: getBunPath(),
args: ['run', customServerPath || getMcpServerPath()],
env: {
AELFSCAN_API_BASE_URL: 'https://aelfscan.io',
},
};
}
export function mergeMcpConfig(
existing: any,
serverName: string,
entry: McpServerEntry,
force = false,
): { config: any; action: 'created' | 'updated' | 'skipped' } {
const config = { ...existing };
if (!config.mcpServers) {
config.mcpServers = {};
}
if (config.mcpServers[serverName] && !force) {
return { config, action: 'skipped' };
}
const action = config.mcpServers[serverName] ? 'updated' : 'created';
config.mcpServers[serverName] = entry;
return { config, action };
}
export function removeMcpEntry(existing: any, serverName: string): { config: any; removed: boolean } {
const config = { ...existing };
if (!config.mcpServers || !config.mcpServers[serverName]) {
return { config, removed: false };
}
delete config.mcpServers[serverName];
return { config, removed: true };
}
export const LOG = {
step: (message: string) => console.log(` -> ${message}`),
info: (message: string) => console.log(` [INFO] ${message}`),
success: (message: string) => console.log(` [OK] ${message}`),
warn: (message: string) => console.log(` [WARN] ${message}`),
error: (message: string) => console.error(` [ERROR] ${message}`),
};
#!/usr/bin/env bun
import './setup.ts';
#!/usr/bin/env bun
import { Command } from 'commander';
import * as fs from 'node:fs';
import packageJson from '../package.json';
import {
LOG,
SERVER_NAME,
getBunPath,
getCursorProjectPath,
getMcpServerPath,
getPackageRoot,
getPlatformPaths,
readJsonFile,
} from './platforms/utils.js';
import { setupClaude, uninstallClaude } from './platforms/claude.js';
import { setupCursor, uninstallCursor } from './platforms/cursor.js';
import { setupOpenClaw, uninstallOpenClaw } from './platforms/openclaw.js';
const program = new Command();
program
.name('aelfscan-setup')
.description('Configure @aelfscan/agent-skills for Claude/Cursor/OpenClaw')
.version(packageJson.version);
const withCommonMcpOptions = (command: Command) =>
command
.option('--server-path <path>', 'Custom path to MCP server.ts')
.option('--config-path <path>', 'Custom config file path')
.option('--force', 'Overwrite existing aelfscan-skill entry', false);
withCommonMcpOptions(
program
.command('claude')
.description('Setup MCP server for Claude Desktop'),
).action(opts => {
console.log('\nConfiguring Claude Desktop...\n');
setupClaude({
configPath: opts.configPath,
serverPath: opts.serverPath,
force: opts.force,
});
console.log('');
});
withCommonMcpOptions(
program
.command('cursor')
.description('Setup MCP server for Cursor')
.option('--global', 'Write to global ~/.cursor/mcp.json', false),
).action(opts => {
console.log(`\nConfiguring Cursor (${opts.global ? 'global' : 'project'})...\n`);
setupCursor({
global: opts.global,
configPath: opts.configPath,
serverPath: opts.serverPath,
force: opts.force,
});
console.log('');
});
program
.command('openclaw')
.description('Generate or merge OpenClaw tool configuration')
.option('--config-path <path>', 'Merge into an existing OpenClaw config file')
.option('--cwd <dir>', 'Working directory for OpenClaw tools')
.option('--force', 'Overwrite existing tools with the same name', false)
.action(opts => {
console.log('\nConfiguring OpenClaw...\n');
setupOpenClaw({
configPath: opts.configPath,
cwd: opts.cwd,
force: opts.force,
});
console.log('');
});
program
.command('list')
.description('Show detected config paths and setup status')
.action(() => {
const pkgRoot = getPackageRoot();
const serverPath = getMcpServerPath();
const bunPath = getBunPath();
const paths = getPlatformPaths();
const cursorProjectPath = getCursorProjectPath();
console.log('\nAelfScan Skill setup status\n');
console.log(` Package root: ${pkgRoot}`);
console.log(` MCP server: ${serverPath} ${fs.existsSync(serverPath) ? '[OK]' : '[MISSING]'}`);
console.log(` Bun path: ${bunPath}`);
console.log('');
const claudeExists = fs.existsSync(paths.claude);
const claude = claudeExists ? readJsonFile(paths.claude) : null;
const claudeConfigured = Boolean(claude?.mcpServers?.[SERVER_NAME]);
console.log(` Claude Desktop: ${paths.claude}`);
console.log(
` Config file: ${claudeExists ? 'exists' : 'not found'} | ${SERVER_NAME}: ${claudeConfigured ? 'configured' : 'not configured'}`,
);
const cursorGlobalExists = fs.existsSync(paths.cursorGlobal);
const cursorGlobal = cursorGlobalExists ? readJsonFile(paths.cursorGlobal) : null;
const cursorGlobalConfigured = Boolean(cursorGlobal?.mcpServers?.[SERVER_NAME]);
console.log(` Cursor (global): ${paths.cursorGlobal}`);
console.log(
` Config file: ${cursorGlobalExists ? 'exists' : 'not found'} | ${SERVER_NAME}: ${cursorGlobalConfigured ? 'configured' : 'not configured'}`,
);
const cursorProjectExists = fs.existsSync(cursorProjectPath);
const cursorProject = cursorProjectExists ? readJsonFile(cursorProjectPath) : null;
const cursorProjectConfigured = Boolean(cursorProject?.mcpServers?.[SERVER_NAME]);
console.log(` Cursor (project): ${cursorProjectPath}`);
console.log(
` Config file: ${cursorProjectExists ? 'exists' : 'not found'} | ${SERVER_NAME}: ${cursorProjectConfigured ? 'configured' : 'not configured'}`,
);
console.log('');
LOG.info('Use `bun run setup claude|cursor|openclaw` to install.');
console.log('');
});
program
.command('uninstall <platform>')
.description('Remove aelfscan-skill setup from platform (claude|cursor|openclaw)')
.option('--global', 'For cursor uninstall global config', false)
.option('--config-path <path>', 'Custom config file path')
.action((platform, opts) => {
console.log(`\nRemoving setup from ${platform}...\n`);
switch (platform) {
case 'claude':
uninstallClaude({ configPath: opts.configPath });
break;
case 'cursor':
uninstallCursor({ global: opts.global, configPath: opts.configPath });
break;
case 'openclaw':
uninstallOpenClaw({ configPath: opts.configPath });
break;
default:
LOG.error(`Unknown platform: ${platform}. Supported: claude, cursor, openclaw.`);
process.exitCode = 1;
break;
}
console.log('');
});
program.parse();
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@aelfscan/agent-skills",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
"commander": "^12.1.0",
"zod": "^3.24.0",
},
"devDependencies": {
"@types/bun": "latest",
"ajv": "^8.17.1",
"typescript": "^5.7.0",
},
},
},
"packages": {
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"body-parser": ["body-parser@2.2.2", "", { "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" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"express": ["express@5.2.1", "", { "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" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"finalhandler": ["finalhandler@2.1.1", "", { "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" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hono": ["hono@4.12.2", "", {}, "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.1", "", { "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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "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" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
}
}
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
"zod": "^3.24.0"
}
}
export * from './lib/types.js';
export * from './lib/api-types.js';
export { getConfig, resetConfigCache } from './lib/config.js';
export { search, getSearchFilters } from './src/core/search.js';
export {
getBlocks,
getLatestBlocks,
getBlockDetail,
getTransactions,
getLatestTransactions,
getTransactionDetail,
getBlockchainOverview,
getTransactionDataChart,
getAddressDictionary,
getLogEvents,
} from './src/core/blockchain.js';
export {
getAccounts,
getContracts,
getAddressDetail,
getAddressTokens,
getAddressNftAssets,
getAddressTransfers,
getContractHistory,
getContractEvents,
getContractSource,
} from './src/core/address.js';
export { getTokens, getTokenDetail, getTokenTransfers, getTokenHolders } from './src/core/token.js';
export {
getNftCollections,
getNftCollectionDetail,
getNftTransfers,
getNftHolders,
getNftInventory,
getNftItemDetail,
getNftItemHolders,
getNftItemActivity,
} from './src/core/nft.js';
export {
getDailyTransactions,
getUniqueAddresses,
getDailyActiveAddresses,
getMonthlyActiveAddresses,
getBlockProduceRate,
getAvgBlockDuration,
getCycleCount,
getNodeBlockProduce,
getDailyAvgTransactionFee,
getDailyTxFee,
getDailyTotalBurnt,
getDailyElfPrice,
getDailyDeployContract,
getDailyBlockReward,
getDailyAvgBlockSize,
getTopContractCall,
getDailyContractCall,
getDailySupplyGrowth,
getDailyMarketCap,
getDailyStaked,
getDailyHolder,
getDailyTvl,
getNodeCurrentProduceInfo,
getElfSupply,
getDailyTransactionInfo,
getDailyActivityAddress,
getCurrencyPrice,
getStatisticsByMetric,
STATISTICS_METRICS,
} from './src/core/statistics.js';
export interface ApiValidationError {
memberNames?: string[];
errorMessage?: string;
}
export interface ApiSuccessEnvelope<T> {
code: string;
message?: string;
data: T;
}
export interface ApiErrorEnvelope {
code: string;
message?: string;
data?: unknown;
validationErrors?: ApiValidationError[];
}
export interface ApiPagedList<TItem> {
total?: number;
list?: TItem[];
items?: TItem[];
[key: string]: unknown;
}
export interface SearchFilterOption {
filterType?: number;
filterInfo?: string;
searchType?: number;
searchInfo?: string;
[key: string]: unknown;
}
export interface SearchFiltersResponse {
filterTypes?: SearchFilterOption[];
searchTypes?: SearchFilterOption[];
[key: string]: unknown;
}
export interface BlockSummary {
blockHeight?: number;
blockHash?: string;
chainId?: string;
blockTime?: string;
txns?: number;
[key: string]: unknown;
}
export interface TransactionSummary {
transactionId?: string;
blockHeight?: number;
blockHash?: string;
from?: string;
to?: string;
methodName?: string;
status?: string;
blockTime?: string;
[key: string]: unknown;
}
export interface LogEvent {
name?: string;
indexed?: unknown[];
nonIndexed?: unknown[];
[key: string]: unknown;
}
export interface SearchEntity {
symbol?: string;
address?: string;
blockHeight?: number;
transactionId?: string;
[key: string]: unknown;
}
export interface SearchResponse {
tokens?: SearchEntity[];
nfts?: SearchEntity[];
accounts?: SearchEntity[];
contracts?: SearchEntity[];
blocks?: BlockSummary[];
block?: BlockSummary;
transaction?: TransactionSummary | null;
[key: string]: unknown;
}
export interface BlockListResponse extends ApiPagedList<BlockSummary> {
blocks?: BlockSummary[];
}
export interface BlockDetailResponse extends BlockSummary {
transactions?: TransactionSummary[];
[key: string]: unknown;
}
export interface TransactionListResponse extends ApiPagedList<TransactionSummary> {
transactions?: TransactionSummary[];
}
export interface TransactionDetailResponse extends TransactionSummary {
logEvents?: LogEvent[] | null;
[key: string]: unknown;
}
export interface BlockchainOverviewResponse {
transactions?: number;
tokenPriceInUsd?: number;
tokenDailyPriceInUsd?: number;
tokenPriceRate24h?: number;
[key: string]: unknown;
}
export interface TransactionChartPoint {
start?: number;
end?: number;
count?: number;
[key: string]: unknown;
}
export interface TransactionDataChartResponse {
all?: TransactionChartPoint[];
mainChain?: TransactionChartPoint[];
sideChain?: TransactionChartPoint[];
[key: string]: unknown;
}
export interface AddressDictionaryResponse {
name?: string;
addresses?: string[];
[key: string]: unknown;
}
export interface LogEventsResponse {
total?: number;
logEvents?: LogEvent[] | null;
[key: string]: unknown;
}
export interface AccountSummary {
address?: string;
balance?: number;
txns?: number;
[key: string]: unknown;
}
export interface ContractSummary {
address?: string;
contractName?: string;
type?: string;
txns?: number;
[key: string]: unknown;
}
export interface AddressDetailResponse {
address?: string;
balance?: number;
accountType?: string;
[key: string]: unknown;
}
export interface AddressAssetItem {
symbol?: string;
balance?: number;
amount?: string | number;
[key: string]: unknown;
}
export interface AddressTransferItem {
transactionId?: string;
symbol?: string;
from?: string;
to?: string;
amount?: string | number;
[key: string]: unknown;
}
export interface ContractHistoryItem {
transactionId?: string;
blockHeight?: number;
updateTime?: string;
[key: string]: unknown;
}
export interface ContractEventItem {
blockHeight?: number;
transactionId?: string;
eventName?: string;
[key: string]: unknown;
}
export interface ContractSourceResponse {
address?: string;
codeHash?: string;
version?: string;
[key: string]: unknown;
}
export interface TokenSummary {
symbol?: string;
tokenName?: string;
decimals?: number;
supply?: string | number;
[key: string]: unknown;
}
export interface TokenDetailResponse extends TokenSummary {
holders?: number;
transfers?: number;
[key: string]: unknown;
}
export interface TokenTransferItem {
transactionId?: string;
from?: string;
to?: string;
amount?: string | number;
[key: string]: unknown;
}
export interface TokenHolderItem {
address?: string;
amount?: string | number;
percentage?: string | number;
[key: string]: unknown;
}
export interface NftCollectionSummary {
collectionSymbol?: string;
collectionName?: string;
items?: number;
holders?: number;
[key: string]: unknown;
}
export interface NftCollectionDetailResponse extends NftCollectionSummary {
description?: string;
[key: string]: unknown;
}
export interface NftTransferItem {
transactionId?: string;
symbol?: string;
from?: string;
to?: string;
[key: string]: unknown;
}
export interface NftHolderItem {
address?: string;
amount?: string | number;
[key: string]: unknown;
}
export interface NftInventoryItem {
symbol?: string;
owner?: string;
[key: string]: unknown;
}
export interface NftItemDetailResponse {
symbol?: string;
collectionSymbol?: string;
owner?: string;
[key: string]: unknown;
}
export interface StatisticsSeriesPoint {
date?: number;
dateStr?: string;
value?: number | string;
[key: string]: unknown;
}
export interface StatisticsListResponse extends ApiPagedList<StatisticsSeriesPoint> {
list?: StatisticsSeriesPoint[];
}
export interface DailyTransactionsPoint extends StatisticsSeriesPoint {
transactionCount?: number;
blockCount?: number;
mergeTransactionCount?: number;
}
export interface DailyTransactionsResponse extends StatisticsListResponse {
list?: DailyTransactionsPoint[];
}
export interface DailyTransactionInfoChain {
transactionAvgByAllType?: number;
transactionAvgByExcludeSystem?: number;
[key: string]: unknown;
}
export interface DailyTransactionInfoResponse {
mainChain?: DailyTransactionInfoChain;
sideChain?: DailyTransactionInfoChain;
[key: string]: unknown;
}
export interface DailyActivityAddressChain {
max?: number;
min?: number;
avg?: number;
[key: string]: unknown;
}
export interface DailyActivityAddressResponse {
mainChain?: DailyActivityAddressChain;
sideChain?: DailyActivityAddressChain;
[key: string]: unknown;
}
export interface NodeCurrentProduceInfoItem {
nodeAddress?: string;
producedBlockCount?: number;
expectedBlockCount?: number;
[key: string]: unknown;
}
export interface NodeCurrentProduceInfoResponse {
roundNumber?: number;
list?: NodeCurrentProduceInfoItem[];
[key: string]: unknown;
}
export interface ElfSupplyResponse {
maxSupply?: number;
burn?: number;
totalSupply?: number;
circulatingSupply?: number;
[key: string]: unknown;
}
import { normalizeChainId } from './normalize.js';
import type { AelfscanConfig } from './types.js';
const DEFAULT_TIMEOUT_MS = 10_000;
const DEFAULT_RETRY = 1;
const DEFAULT_API_BASE_URL = 'https://aelfscan.io';
const DEFAULT_RETRY_BASE_MS = 200;
const DEFAULT_RETRY_MAX_MS = 3_000;
const DEFAULT_MAX_CONCURRENT_REQUESTS = 5;
const DEFAULT_CACHE_TTL_MS = 60_000;
const DEFAULT_CACHE_MAX_ENTRIES = 500;
const DEFAULT_MAX_RESULT_COUNT = 200;
const DEFAULT_MCP_MAX_ITEMS = 50;
const DEFAULT_MCP_MAX_CHARS = 60_000;
const DEFAULT_MCP_INCLUDE_RAW = false;
let cachedConfig: AelfscanConfig | null = null;
function toNumber(raw: string | undefined, fallback: number): number {
if (!raw) {
return fallback;
}
const value = Number(raw);
return Number.isFinite(value) && value >= 0 ? value : fallback;
}
function toBoolean(raw: string | undefined, fallback: boolean): boolean {
if (!raw) {
return fallback;
}
const normalized = raw.trim().toLowerCase();
if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') {
return true;
}
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') {
return false;
}
return fallback;
}
function trimSlash(input: string): string {
return input.replace(/\/+$/, '');
}
export function getConfig(): AelfscanConfig {
if (cachedConfig) {
return cachedConfig;
}
cachedConfig = {
apiBaseUrl: trimSlash(process.env.AELFSCAN_API_BASE_URL || DEFAULT_API_BASE_URL),
defaultChainId: normalizeChainId(process.env.AELFSCAN_DEFAULT_CHAIN_ID || ''),
timeoutMs: toNumber(process.env.AELFSCAN_TIMEOUT_MS, DEFAULT_TIMEOUT_MS),
retry: Math.floor(toNumber(process.env.AELFSCAN_RETRY, DEFAULT_RETRY)),
retryBaseMs: Math.floor(toNumber(process.env.AELFSCAN_RETRY_BASE_MS, DEFAULT_RETRY_BASE_MS)),
retryMaxMs: Math.floor(toNumber(process.env.AELFSCAN_RETRY_MAX_MS, DEFAULT_RETRY_MAX_MS)),
maxConcurrentRequests: Math.floor(
toNumber(process.env.AELFSCAN_MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_CONCURRENT_REQUESTS),
),
cacheTtlMs: Math.floor(toNumber(process.env.AELFSCAN_CACHE_TTL_MS, DEFAULT_CACHE_TTL_MS)),
cacheMaxEntries: Math.floor(toNumber(process.env.AELFSCAN_CACHE_MAX_ENTRIES, DEFAULT_CACHE_MAX_ENTRIES)),
maxResultCount: Math.floor(toNumber(process.env.AELFSCAN_MAX_RESULT_COUNT, DEFAULT_MAX_RESULT_COUNT)),
mcpMaxItems: Math.floor(toNumber(process.env.AELFSCAN_MCP_MAX_ITEMS, DEFAULT_MCP_MAX_ITEMS)),
mcpMaxChars: Math.floor(toNumber(process.env.AELFSCAN_MCP_MAX_CHARS, DEFAULT_MCP_MAX_CHARS)),
mcpIncludeRaw: toBoolean(process.env.AELFSCAN_MCP_INCLUDE_RAW, DEFAULT_MCP_INCLUDE_RAW),
};
return cachedConfig;
}
export function resetConfigCache(): void {
cachedConfig = null;
}
import type { ToolError, ToolResult } from './types.js';
export class SkillError extends Error {
public readonly code: string;
public readonly details?: unknown;
public readonly httpStatus?: number;
constructor(code: string, message: string, details?: unknown, httpStatus?: number) {
super(message);
this.name = 'SkillError';
this.code = code;
this.details = details;
this.httpStatus = httpStatus;
}
}
export class HttpStatusError extends SkillError {
constructor(status: number, body: unknown) {
super('HTTP_ERROR', `HTTP request failed with status ${status}`, body, status);
this.name = 'HttpStatusError';
}
}
export function toToolError(err: unknown, fallbackCode = 'UNKNOWN_ERROR'): ToolError {
if (err instanceof SkillError) {
return {
code: err.code,
message: err.message,
details: err.details,
httpStatus: err.httpStatus,
};
}
if (err instanceof Error) {
return {
code: fallbackCode,
message: err.message,
};
}
return {
code: fallbackCode,
message: String(err),
};
}
export function ok<T>(traceId: string, data: T, raw?: unknown): ToolResult<T> {
return {
success: true,
data,
traceId,
raw,
};
}
export function fail<T>(traceId: string, err: unknown, fallbackCode = 'UNKNOWN_ERROR', raw?: unknown): ToolResult<T> {
return {
success: false,
error: toToolError(err, fallbackCode),
traceId,
raw,
};
}
export function requireField<T>(value: T | null | undefined, name: string): T {
if (value === undefined || value === null || value === '') {
throw new SkillError('INVALID_INPUT', `${name} is required`);
}
return value;
}
import { getConfig } from './config.js';
import { SkillError, HttpStatusError } from './errors.js';
import { serializeQuery } from './serializer.js';
import type { AelfscanEnvelope, HttpClientResult } from './types.js';
export interface HttpRequestOptions {
method?: 'GET' | 'POST';
path: string;
query?: Record<string, unknown>;
body?: unknown;
headers?: Record<string, string>;
traceId?: string;
cacheTtlMs?: number;
disableCache?: boolean;
}
const responseCache = new Map<string, { expiresAt: number; value: HttpClientResult<unknown> }>();
const pendingResolvers: Array<() => void> = [];
let activeRequests = 0;
function shouldRetry(error: unknown): boolean {
if (!(error instanceof SkillError)) {
return true;
}
if (!error.httpStatus) {
return true;
}
return error.httpStatus >= 500;
}
function getRetryDelayMs(attempt: number, retryBaseMs: number, retryMaxMs: number): number {
const exponential = Math.min(retryBaseMs * 2 ** attempt, retryMaxMs);
const jitter = Math.floor(Math.random() * Math.max(1, Math.floor(exponential * 0.25)));
return exponential + jitter;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function parseResponseBody(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch {
return text;
}
}
function ensurePath(path: string): string {
if (path.startsWith('/')) {
return path;
}
return `/${path}`;
}
async function acquireSlot(limit: number): Promise<() => void> {
if (limit <= 0) {
return () => {};
}
if (activeRequests < limit) {
activeRequests += 1;
return releaseSlot;
}
await new Promise<void>((resolve) => pendingResolvers.push(resolve));
activeRequests += 1;
return releaseSlot;
}
function releaseSlot(): void {
activeRequests = Math.max(0, activeRequests - 1);
const next = pendingResolvers.shift();
if (next) {
next();
}
}
function getCacheTtlMs(method: 'GET' | 'POST', path: string, options: HttpRequestOptions): number {
const config = getConfig();
if (options.disableCache) {
return 0;
}
if (options.cacheTtlMs !== undefined) {
return Math.max(0, options.cacheTtlMs);
}
if (method === 'GET' && path.startsWith('/api/app/statistics/')) {
return Math.max(0, config.cacheTtlMs);
}
return 0;
}
function getCacheKey(method: 'GET' | 'POST', url: string): string {
return `${method}:${url}`;
}
function getCachedValue<T>(cacheKey: string): HttpClientResult<T> | null {
const cached = responseCache.get(cacheKey);
if (!cached) {
return null;
}
if (cached.expiresAt <= Date.now()) {
responseCache.delete(cacheKey);
return null;
}
// Keep most recently used cache item at the tail.
responseCache.delete(cacheKey);
responseCache.set(cacheKey, cached);
return cached.value as HttpClientResult<T>;
}
function setCachedValue(cacheKey: string, value: HttpClientResult<unknown>, expiresAt: number, maxEntries: number): void {
if (maxEntries <= 0) {
return;
}
if (responseCache.has(cacheKey)) {
responseCache.delete(cacheKey);
}
responseCache.set(cacheKey, { expiresAt, value });
while (responseCache.size > maxEntries) {
const oldest = responseCache.keys().next().value;
if (oldest === undefined) {
break;
}
responseCache.delete(oldest);
}
}
export function resetHttpClientState(): void {
responseCache.clear();
activeRequests = 0;
while (pendingResolvers.length > 0) {
const next = pendingResolvers.shift();
if (next) {
next();
}
}
}
export async function request<T>(options: HttpRequestOptions): Promise<HttpClientResult<T>> {
const config = getConfig();
const method = options.method || 'GET';
const path = ensurePath(options.path);
const queryString = serializeQuery(options.query);
const url = `${config.apiBaseUrl}${path}${queryString ? `?${queryString}` : ''}`;
const traceId = options.traceId;
const cacheTtlMs = getCacheTtlMs(method, path, options);
const cacheMaxEntries = Math.max(0, config.cacheMaxEntries);
const cacheKey = getCacheKey(method, url);
if (cacheTtlMs > 0 && cacheMaxEntries > 0 && method === 'GET') {
const cached = getCachedValue<T>(cacheKey);
if (cached) {
return cached;
}
}
let lastError: unknown;
for (let attempt = 0; attempt <= config.retry; attempt += 1) {
const releaseSlotHandle = await acquireSlot(config.maxConcurrentRequests);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
try {
const response = await fetch(url, {
method,
signal: controller.signal,
headers: {
Accept: 'application/json, text/plain; q=0.9',
'Content-Type': 'application/json',
...(traceId ? { 'X-Trace-Id': traceId } : {}),
...(options.headers || {}),
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
const rawBody = await parseResponseBody(response);
if (!response.ok) {
throw new HttpStatusError(response.status, rawBody);
}
const envelope = rawBody as AelfscanEnvelope<T>;
if (envelope && typeof envelope === 'object' && 'code' in envelope) {
if (envelope.code && envelope.code !== '20000') {
throw new SkillError('API_BUSINESS_ERROR', envelope.message || 'Aelfscan business error', envelope);
}
const successResult = {
data: (envelope.data as T) ?? ({} as T),
raw: envelope,
};
if (cacheTtlMs > 0 && cacheMaxEntries > 0 && method === 'GET') {
setCachedValue(cacheKey, successResult as HttpClientResult<unknown>, Date.now() + cacheTtlMs, cacheMaxEntries);
}
return successResult;
}
const successResult = {
data: rawBody as T,
raw: rawBody,
};
if (cacheTtlMs > 0 && cacheMaxEntries > 0 && method === 'GET') {
setCachedValue(cacheKey, successResult as HttpClientResult<unknown>, Date.now() + cacheTtlMs, cacheMaxEntries);
}
return successResult;
} catch (error) {
lastError = error;
if (attempt < config.retry && shouldRetry(error)) {
const delayMs = getRetryDelayMs(attempt, config.retryBaseMs, config.retryMaxMs);
await sleep(delayMs);
continue;
}
break;
} finally {
clearTimeout(timeout);
releaseSlotHandle();
}
}
throw lastError;
}
const ADDRESS_WITH_PREFIX = /^(?:ELF_)?(.+?)(?:_[^_]+)?$/;
export function normalizeChainId(input?: string): string {
if (!input) {
return '';
}
const trimmed = input.trim();
if (!trimmed) {
return '';
}
if (trimmed === 'multiChain') {
return '';
}
return trimmed;
}
export function normalizeAddress(input?: string): string {
if (!input) {
return '';
}
const trimmed = input.trim();
if (!trimmed) {
return '';
}
const match = trimmed.match(ADDRESS_WITH_PREFIX);
if (!match || !match[1]) {
return trimmed;
}
return match[1];
}
function pushQueryPart(parts: string[], key: string, value: unknown): void {
if (value === undefined || value === null) {
return;
}
if (value === '') {
parts.push(`${key}=`);
return;
}
parts.push(`${key}=${encodeURIComponent(String(value))}`);
}
function processObject(parts: string[], source: Record<string, unknown>, prefix?: string, isObjectItem = false): void {
Object.keys(source).forEach((key) => {
const value = source[key];
const prefixedKey = prefix ? (isObjectItem ? `${prefix}.${key}` : `${prefix}[${key}]`) : key;
if (Array.isArray(value)) {
value.forEach((item, index) => {
if (item && typeof item === 'object') {
processObject(parts, item as Record<string, unknown>, `${prefixedKey}[${index}]`, true);
} else {
pushQueryPart(parts, `${prefixedKey}[${index}]`, item);
}
});
return;
}
if (value && typeof value === 'object') {
processObject(parts, value as Record<string, unknown>, prefixedKey, false);
return;
}
if (value === undefined || value === null) {
return;
}
if (typeof value === 'number' && Number.isNaN(value)) {
return;
}
// Primitive values reach here after null/undefined/object filtering.
pushQueryPart(parts, prefixedKey, value);
});
}
export function serializeQuery(params?: Record<string, unknown>): string {
if (!params) {
return '';
}
const keys = Object.keys(params);
if (keys.length === 0) {
return '';
}
const parts: string[] = [];
processObject(parts, params);
if (parts.length === 0) {
return '';
}
return parts.join('&');
}
export function createTraceId(): string {
const time = Date.now().toString(36);
const random = Math.random().toString(36).slice(2, 10);
return `${time}-${random}`;
}
export type SortDirection = 'Asc' | 'Desc';
export interface OrderInfo {
orderBy: string;
sort: SortDirection;
}
export interface PaginationInput {
chainId?: string;
skipCount?: number;
maxResultCount?: number;
orderBy?: string;
sort?: SortDirection;
orderInfos?: OrderInfo[];
searchAfter?: string[];
}
export interface ToolError {
code: string;
message: string;
details?: unknown;
httpStatus?: number;
}
export interface ToolResult<T> {
success: boolean;
data?: T;
error?: ToolError;
traceId: string;
raw?: unknown;
}
export interface AelfscanEnvelope<T> {
code?: string;
message?: string;
data?: T;
}
export interface HttpClientResult<T> {
data: T;
raw: unknown;
}
export interface AelfscanConfig {
apiBaseUrl: string;
defaultChainId: string;
timeoutMs: number;
retry: number;
retryBaseMs: number;
retryMaxMs: number;
maxConcurrentRequests: number;
cacheTtlMs: number;
cacheMaxEntries: number;
maxResultCount: number;
mcpMaxItems: number;
mcpMaxChars: number;
mcpIncludeRaw: boolean;
}
export interface SearchInput {
chainId?: string;
keyword: string;
filterType?: number;
searchType?: number;
}
export interface SearchFiltersInput {
chainId?: string;
}
export interface BlocksInput extends PaginationInput {
isLastPage?: boolean;
}
export interface BlockDetailInput {
chainId?: string;
blockHeight: number;
}
export interface TransactionsInput extends PaginationInput {
transactionId?: string;
blockHeight?: number;
address?: string;
startTime?: number;
endTime?: number;
}
export interface TransactionDetailInput {
chainId?: string;
transactionId: string;
blockHeight?: number;
}
export interface BlockchainOverviewInput {
chainId?: string;
[key: string]: unknown;
}
export interface TransactionDataChartInput {
chainId?: string;
[key: string]: unknown;
}
export interface AddressDictionaryInput {
chainId?: string;
name: string;
addresses: string[];
[key: string]: unknown;
}
export interface LogEventsInput extends PaginationInput {
chainId?: string;
contractAddress: string;
address?: string;
eventName?: string;
transactionId?: string;
blockHeight?: number;
startBlockHeight?: number;
endBlockHeight?: number;
[key: string]: unknown;
}
export interface AccountsInput extends PaginationInput {}
export interface ContractsInput extends PaginationInput {}
export interface AddressDetailInput {
chainId?: string;
address: string;
}
export interface AddressTokensInput extends PaginationInput {
address: string;
fuzzySearch?: string;
}
export interface AddressNftAssetsInput extends PaginationInput {
address: string;
fuzzySearch?: string;
}
export interface AddressTransfersInput extends PaginationInput {
address: string;
symbol?: string;
tokenType?: number;
}
export interface ContractHistoryInput {
chainId?: string;
address: string;
}
export interface ContractEventsInput extends PaginationInput {
chainId?: string;
contractAddress: string;
blockHeight?: number;
}
export interface ContractSourceInput {
chainId?: string;
address: string;
}
export interface TokenListInput extends PaginationInput {
types?: number[];
symbols?: string[];
collectionSymbols?: string[];
search?: string;
exactSearch?: string;
fuzzySearch?: string;
beginBlockTime?: string | number;
}
export interface TokenDetailInput {
chainId?: string;
symbol: string;
}
export interface TokenTransfersInput extends PaginationInput {
symbol: string;
search?: string;
collectionSymbol?: string;
address?: string;
types?: number[];
fuzzySearch?: string;
beginBlockTime?: string | number;
}
export interface TokenHoldersInput extends PaginationInput {
symbol?: string;
collectionSymbol?: string;
address?: string;
partialSymbol?: string;
search?: string;
types?: number[];
symbols?: string[];
addressList?: string[];
searchSymbols?: string[];
fuzzySearch?: string;
amountGreaterThanZero?: boolean;
}
export interface NftCollectionsInput extends TokenListInput {}
export interface NftCollectionDetailInput {
chainId?: string;
collectionSymbol: string;
}
export interface NftTransfersInput extends PaginationInput {
chainId?: string;
collectionSymbol: string;
search?: string;
address?: string;
}
export interface NftHoldersInput extends PaginationInput {
chainId?: string;
collectionSymbol: string;
search?: string;
}
export interface NftInventoryInput extends PaginationInput {
chainId?: string;
collectionSymbol: string;
search?: string;
}
export interface NftItemDetailInput {
chainId?: string;
symbol: string;
}
export interface NftItemHoldersInput extends PaginationInput {
chainId?: string;
symbol: string;
types?: number[];
}
export interface NftItemActivityInput extends PaginationInput {
chainId?: string;
symbol: string;
}
export interface StatisticsQueryInput {
chainId?: string;
startDate?: string;
endDate?: string;
[key: string]: unknown;
}
export interface StatisticsDateRangeInput extends StatisticsQueryInput {
startDate: string;
endDate: string;
}
export type StatisticsMetric =
| 'dailyTransactions'
| 'uniqueAddresses'
| 'dailyActiveAddresses'
| 'monthlyActiveAddresses'
| 'blockProduceRate'
| 'avgBlockDuration'
| 'cycleCount'
| 'nodeBlockProduce'
| 'dailyAvgTransactionFee'
| 'dailyTxFee'
| 'dailyTotalBurnt'
| 'dailyElfPrice'
| 'dailyDeployContract'
| 'dailyBlockReward'
| 'dailyAvgBlockSize'
| 'topContractCall'
| 'dailyContractCall'
| 'dailySupplyGrowth'
| 'dailyMarketCap'
| 'dailyStaked'
| 'dailyHolder'
| 'dailyTvl'
| 'nodeCurrentProduceInfo'
| 'elfSupply'
| 'dailyTransactionInfo'
| 'dailyActivityAddress'
| 'currencyPrice';
export interface StatisticsMetricInput extends StatisticsQueryInput {
metric: StatisticsMetric;
}
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"mcpServers": {
"aelfscan-skill": {
"command": "bun",
"args": ["run", "/ABSOLUTE/PATH/TO/src/mcp/server.ts"],
"env": {
"AELFSCAN_API_BASE_URL": "https://aelfscan.io"
}
}
}
}
{
"name": "aelfscan-skill",
"description": "AelfScan explorer tools for search, blockchain, addresses, tokens, NFTs, and statistics.",
"tools": [
{
"name": "aelfscan_search_filters",
"description": "Get search filter metadata used by explorer search UI.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"search",
"filters"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_search",
"description": "Search tokens/accounts/contracts/NFTs/blocks/transactions on AelfScan explorer.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"search",
"query"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_blocks",
"description": "List blocks with pagination.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"blocks"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_blocks_latest",
"description": "Get latest blocks (uses blocks API with skipCount=0).",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"blocks-latest"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_block_detail",
"description": "Get block detail by block height.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"block-detail"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_transactions",
"description": "List transactions with optional filters.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"transactions"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_transactions_latest",
"description": "Get latest transactions (uses transactions API with skipCount=0).",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"transactions-latest"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_transaction_detail",
"description": "Get transaction detail by transaction id.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"transaction-detail"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_blockchain_overview",
"description": "Get blockchain overview metrics and aggregate stats.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"overview"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_transaction_data_chart",
"description": "Get transaction data chart series.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"transaction-data-chart"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_address_dictionary",
"description": "Resolve or query address dictionary metadata.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"address-dictionary"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_log_events",
"description": "Get contract log events by contract address.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"blockchain",
"log-events"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_accounts",
"description": "List top accounts.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"accounts"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_contracts",
"description": "List contracts.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"contracts"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_address_detail",
"description": "Get address detail (EOA/contract profile and portfolio).",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"detail"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_address_tokens",
"description": "Get token holdings for an address.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"tokens"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_address_nft_assets",
"description": "Get NFT holdings for an address.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"nft-assets"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_address_transfers",
"description": "Get transfer history for an address.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"transfers"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_contract_history",
"description": "Get contract deploy/update history.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"contract-history"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_contract_events",
"description": "Get contract events list.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"contract-events"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_contract_source",
"description": "Get verified contract source metadata.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"address",
"contract-source"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_tokens",
"description": "List tokens.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"token",
"list"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_token_detail",
"description": "Get token detail by symbol.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"token",
"detail"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_token_transfers",
"description": "Get token transfer list by symbol.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"token",
"transfers"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_token_holders",
"description": "Get token holders.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"token",
"holders"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_collections",
"description": "List NFT collections.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"collections"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_collection_detail",
"description": "Get NFT collection detail.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"collection-detail"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_transfers",
"description": "Get NFT transfers by collection symbol.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"transfers"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_holders",
"description": "Get NFT holders by collection symbol.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"holders"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_inventory",
"description": "Get NFT inventory by collection symbol.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"inventory"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_item_detail",
"description": "Get NFT item detail by symbol.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"item-detail"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_item_holders",
"description": "Get holders of a specific NFT item.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"item-holders"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_nft_item_activity",
"description": "Get activity list of a specific NFT item.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"nft",
"item-activity"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics",
"description": "Get statistics by metric enum, supports all existing statistics endpoints.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"metric"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_transactions",
"description": "Get daily transactions statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-transactions"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_unique_addresses",
"description": "Get unique addresses statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"unique-addresses"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_active_addresses",
"description": "Get daily active addresses statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-active-addresses"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_monthly_active_addresses",
"description": "Get monthly active addresses statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"monthly-active-addresses"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_block_produce_rate",
"description": "Get block produce rate statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"block-produce-rate"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_avg_block_duration",
"description": "Get average block duration statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"avg-block-duration"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_cycle_count",
"description": "Get cycle count statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"cycle-count"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_node_block_produce",
"description": "Get node block produce statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"node-block-produce"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_avg_transaction_fee",
"description": "Get daily average transaction fee statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-avg-transaction-fee"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_tx_fee",
"description": "Get daily transaction fee statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-tx-fee"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_total_burnt",
"description": "Get daily total burnt statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-total-burnt"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_elf_price",
"description": "Get daily ELF price statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-elf-price"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_deploy_contract",
"description": "Get daily deploy contract statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-deploy-contract"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_block_reward",
"description": "Get daily block reward statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-block-reward"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_avg_block_size",
"description": "Get daily average block size statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-avg-block-size"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_top_contract_call",
"description": "Get top contract call statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"top-contract-call"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_contract_call",
"description": "Get daily contract call statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-contract-call"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_supply_growth",
"description": "Get daily supply growth statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-supply-growth"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_market_cap",
"description": "Get daily market cap statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-market-cap"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_staked",
"description": "Get daily staked statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-staked"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_holder",
"description": "Get daily holder statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-holder"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_tvl",
"description": "Get daily TVL statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-tvl"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_node_current_produce_info",
"description": "Get current node produce information.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"node-current-produce-info"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_elf_supply",
"description": "Get ELF supply statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"elf-supply"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_transaction_info",
"description": "Get daily transaction summary for a date range.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-transaction-info"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_daily_activity_address",
"description": "Get daily activity address summary for a date range.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"daily-activity-address"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
{
"name": "aelfscan_statistics_currency_price",
"description": "Get currency price statistics.",
"command": "bun",
"args": [
"run",
"aelfscan_skill.ts",
"statistics",
"currency-price"
],
"cwd": ".",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true
}
}
]
}
{
"name": "@aelfscan/agent-skills",
"version": "0.2.2",
"description": "AelfScan explorer skill toolkit for AI agents: MCP, CLI, and SDK interfaces.",
"type": "module",
"main": "index.ts",
"exports": {
".": "./index.ts",
"./mcp": "./src/mcp/server.ts"
},
"bin": {
"aelfscan-skill": "./aelfscan_skill.ts",
"aelfscan-setup": "./bin/setup.js"
},
"files": [
"index.ts",
"src/",
"lib/",
"bin/",
"aelfscan_skill.ts",
"openclaw.json",
"mcp-config.example.json",
"README.md",
"README.zh-CN.md",
"LICENSE",
".env.example"
],
"scripts": {
"setup": "bun run bin/setup.ts",
"mcp": "bun run src/mcp/server.ts",
"cli": "bun run aelfscan_skill.ts",
"build:openclaw": "bun run bin/generate-openclaw.ts",
"build:openclaw:check": "bun run bin/generate-openclaw.ts --check",
"test": "bun test tests/",
"test:unit": "bun test tests/unit/",
"test:unit:coverage": "bun run test:unit --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage",
"coverage:badge": "bun run bin/generate-coverage-badge.ts",
"test:integration": "bun test tests/integration/",
"test:e2e": "bun test tests/e2e/",
"coverage:gate": "bun run scripts/coverage-gate.ts",
"test:coverage:ci": "COVERAGE_MIN_LINES=85 COVERAGE_MIN_FUNCS=80 bun run test:unit:coverage && bun run coverage:gate",
"deps:check": "bun run scripts/check-deps-baseline.ts"
},
"keywords": [
"aelfscan",
"aelf",
"mcp",
"agent",
"blockchain",
"sdk",
"skill",
"explorer"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/AelfScanProject/aelfscan-skill.git"
},
"homepage": "https://github.com/AelfScanProject/aelfscan-skill#readme",
"bugs": {
"url": "https://github.com/AelfScanProject/aelfscan-skill/issues"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
"commander": "^12.1.0",
"zod": "^3.24.0"
},
"devDependencies": {
"@types/bun": "latest",
"ajv": "^8.17.1",
"typescript": "^5.7.0"
}
}
AelfScan Skill Kit
English | 中文
 
AelfScan explorer skill toolkit for AI agents, with SDK + MCP + CLI + OpenClaw interfaces.
Features
- Search: filters and multi-entity search (tokens/accounts/contracts/NFTs/blocks/transactions)
- Blockchain: blocks, latest blocks, transactions, latest transactions, block detail, transaction detail, overview/chart/address dictionary/log events
- Address: accounts, contracts, address detail, token/NFT assets, transfers, contract history/events/source
- Token: token list/detail/transfers/holders
- NFT: collections/detail/transfers/holders/inventory/item detail/item holders/item activity
- Statistics: daily transactions/addresses/activity, produce metrics, fees/reward/burn, supply/market/staking/TVL, node/ELF supply and date-range summary APIs
- Metadata-driven tool registry: one source of truth for SDK/CLI/MCP/OpenClaw
- MCP output governance: array truncation + max chars + configurable raw payload inclusion
- Unified output shape:
ToolResult<T>withtraceId, standardized errors, andrawpayload
Architecture
aelfscan-skill/
├── index.ts # SDK exports
├── aelfscan_skill.ts # CLI adapter
├── src/
│ ├── core/ # Domain logic (search/blockchain/address/token/nft/statistics)
│ ├── tooling/ # Single source tool descriptors
│ └── mcp/ # MCP adapter + output policy
├── lib/ # Config/http/errors/trace/types
├── bin/setup.ts # Setup for claude/cursor/openclaw
├── openclaw.json
├── mcp-config.example.json
└── tests/ # unit/integration/e2eQuick Start
Install
bun installConfigure
cp .env.example .envRun MCP
bun run mcpOpenClaw
bun run build:openclaw
bun run build:openclaw:checkRun CLI
bun run aelfscan_skill.ts search query --input '{"chainId":"AELF","keyword":"ELF","filterType":0,"searchType":0}'
bun run aelfscan_skill.ts blockchain blocks --input '{"chainId":"AELF","maxResultCount":2,"skipCount":0}'
bun run aelfscan_skill.ts blockchain overview --input '{"chainId":"AELF"}'
bun run aelfscan_skill.ts blockchain log-events --input '{"chainId":"AELF","contractAddress":"256MtWxt3dvxBUdh1XHjQeeSDm2SMR98gDQxLL4UXjwFDhzcAM","maxResultCount":1,"skipCount":0}'
bun run aelfscan_skill.ts address detail --input '{"chainId":"AELF","address":"JRmBduh4nXWi1aXgdUsj5gJrzeZb2LxmrAbf7W99faZSvoAaE"}'
bun run aelfscan_skill.ts statistics daily-transactions --input '{"chainId":"AELF"}'
bun run aelfscan_skill.ts statistics daily-transaction-info --input '{"chainId":"AELF","startDate":"2026-02-20","endDate":"2026-02-26"}'
bun run aelfscan_skill.ts statistics metric --input '{"metric":"dailyTransactions","chainId":"AELF"}'MCP Config Example
Use `mcp-config.example.json`.
Setup Helper
bun run setup claude
bun run setup cursor
bun run setup cursor --global
bun run setup openclaw
bun run setup list
bun run build:openclawTests
bun run test:unit
bun run test:unit:coverage
bun run coverage:badge
bun run test:integration
bun run test:e2e
# live smoke (optional)
RUN_LIVE_TESTS=1 bun run test:e2eEnvironment Variables
AELFSCAN_API_BASE_URL(default:https://aelfscan.io)AELFSCAN_DEFAULT_CHAIN_ID(default: empty for multi-chain)AELFSCAN_TIMEOUT_MS(default:10000)AELFSCAN_RETRY(default:1)AELFSCAN_RETRY_BASE_MS(default:200)AELFSCAN_RETRY_MAX_MS(default:3000)AELFSCAN_MAX_CONCURRENT_REQUESTS(default:5)AELFSCAN_CACHE_TTL_MS(default:60000)AELFSCAN_CACHE_MAX_ENTRIES(default:500)AELFSCAN_MAX_RESULT_COUNT(default:200)AELFSCAN_MCP_MAX_ITEMS(default:50)AELFSCAN_MCP_MAX_CHARS(default:60000)AELFSCAN_MCP_INCLUDE_RAW(default:false)
Wallet Context Compatibility
- This skill is read-only and does not consume signer/private-key context for on-chain writes.
- It is compatible with the shared wallet-context protocol (
~/.portkey/skill-wallet/context.v1.json) used by write-capable skills. bun run deps:checkvalidates wallet-context schema version when a local context file exists.
License
MIT
Security
- Keep API tokens and private keys in env/config only.
- Never leak secret values in tool outputs.
AelfScan Skill Kit
中文 | English
 
面向 AI Agent 的 AelfScan 浏览器能力工具包,提供 SDK + MCP + CLI + OpenClaw 四种使用方式。
功能覆盖
- Search:筛选器与多实体搜索(token/account/contract/NFT/block/transaction)
- Blockchain:区块列表、最新区块、交易列表、最新交易、区块详情、交易详情、overview/chart/address dictionary/log events
- Address:账户/合约列表,地址详情,Token/NFT 资产,转账记录,合约历史/事件/源码
- Token:列表、详情、转账、持有人
- NFT:合集列表/详情、转账、持有人、库存、Item 详情/持有人/活动
- Statistics:交易/地址活跃度、产块指标、手续费/奖励/销毁、供给/市值/质押/TVL、节点与 ELF 供给、按日期区间汇总
- 单一元数据源:SDK/CLI/MCP/OpenClaw 共用 tool descriptor
- MCP 输出治理:数组截断 + 文本长度上限 +
raw可配置 - 统一返回模型:
ToolResult<T>,包含traceId、标准化错误和raw原始响应
架构
aelfscan-skill/
├── index.ts # SDK 导出
├── aelfscan_skill.ts # CLI 适配层
├── src/
│ ├── core/ # 域逻辑(search/blockchain/address/token/nft/statistics)
│ ├── tooling/ # Tool descriptor 单一真源
│ └── mcp/ # MCP 适配层与输出治理
├── lib/ # config/http/errors/trace/types
├── bin/setup.ts # claude/cursor/openclaw 一键配置
├── openclaw.json
├── mcp-config.example.json
└── tests/ # unit/integration/e2e快速开始
安装
bun install环境变量
cp .env.example .env启动 MCP
bun run mcpOpenClaw
bun run build:openclaw
bun run build:openclaw:checkCLI 示例
bun run aelfscan_skill.ts search query --input '{"chainId":"AELF","keyword":"ELF","filterType":0,"searchType":0}'
bun run aelfscan_skill.ts blockchain blocks --input '{"chainId":"AELF","maxResultCount":2,"skipCount":0}'
bun run aelfscan_skill.ts blockchain overview --input '{"chainId":"AELF"}'
bun run aelfscan_skill.ts blockchain log-events --input '{"chainId":"AELF","contractAddress":"256MtWxt3dvxBUdh1XHjQeeSDm2SMR98gDQxLL4UXjwFDhzcAM","maxResultCount":1,"skipCount":0}'
bun run aelfscan_skill.ts address detail --input '{"chainId":"AELF","address":"JRmBduh4nXWi1aXgdUsj5gJrzeZb2LxmrAbf7W99faZSvoAaE"}'
bun run aelfscan_skill.ts statistics daily-transactions --input '{"chainId":"AELF"}'
bun run aelfscan_skill.ts statistics daily-transaction-info --input '{"chainId":"AELF","startDate":"2026-02-20","endDate":"2026-02-26"}'
bun run aelfscan_skill.ts statistics metric --input '{"metric":"dailyTransactions","chainId":"AELF"}'MCP 配置模板
参考 `mcp-config.example.json`。
一键配置命令
bun run setup claude
bun run setup cursor
bun run setup cursor --global
bun run setup openclaw
bun run setup list
bun run build:openclaw测试
bun run test:unit
bun run test:unit:coverage
bun run coverage:badge
bun run test:integration
bun run test:e2e
# 可选:线上只读烟测
RUN_LIVE_TESTS=1 bun run test:e2e环境变量说明
AELFSCAN_API_BASE_URL(默认https://aelfscan.io)AELFSCAN_DEFAULT_CHAIN_ID(默认空字符串,表示 multi-chain)AELFSCAN_TIMEOUT_MS(默认10000)AELFSCAN_RETRY(默认1)AELFSCAN_RETRY_BASE_MS(默认200)AELFSCAN_RETRY_MAX_MS(默认3000)AELFSCAN_MAX_CONCURRENT_REQUESTS(默认5)AELFSCAN_CACHE_TTL_MS(默认60000)AELFSCAN_CACHE_MAX_ENTRIES(默认500)AELFSCAN_MAX_RESULT_COUNT(默认200)AELFSCAN_MCP_MAX_ITEMS(默认50)AELFSCAN_MCP_MAX_CHARS(默认60000)AELFSCAN_MCP_INCLUDE_RAW(默认false)
钱包上下文兼容性
- 本 skill 为只读,不消费 signer/private-key 上下文,也不执行链上写操作。
- 兼容写能力 skill 使用的共享 wallet-context 协议(
~/.portkey/skill-wallet/context.v1.json)。 - 当本地存在 context 文件时,
bun run deps:check会校验 wallet-context schema 版本。
License
MIT
安全
- API Token 和私钥仅通过环境变量或配置注入。
- 工具输出中禁止泄露敏感字段。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/AElfProject/aelf-skills/docs/schemas/wallet-context.v1.schema.json",
"title": "WalletContextFileV1",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"activeProfileId",
"profiles",
"lastWriter"
],
"properties": {
"version": {
"type": "integer",
"const": 1
},
"activeProfileId": {
"type": "string",
"minLength": 1
},
"profiles": {
"type": "object",
"minProperties": 0,
"additionalProperties": {
"$ref": "#/$defs/activeProfile"
}
},
"lastWriter": {
"$ref": "#/$defs/lastWriter"
}
},
"$defs": {
"activeProfile": {
"type": "object",
"additionalProperties": false,
"required": [
"walletType",
"source",
"updatedAt"
],
"properties": {
"walletType": {
"type": "string",
"enum": [
"EOA",
"CA"
]
},
"source": {
"type": "string",
"enum": [
"eoa-local",
"ca-keystore",
"env"
]
},
"network": {
"type": "string"
},
"address": {
"type": "string"
},
"caAddress": {
"type": "string"
},
"caHash": {
"type": "string"
},
"walletFile": {
"type": "string"
},
"keystoreFile": {
"type": "string"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
}
},
"lastWriter": {
"type": "object",
"additionalProperties": false,
"required": [
"skill",
"version"
],
"properties": {
"skill": {
"type": "string",
"minLength": 1
},
"version": {
"type": "string",
"minLength": 1
}
}
}
}
}
#!/usr/bin/env bun
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { resolve } from 'node:path';
import Ajv from 'ajv';
type Baseline = {
dependencies: Record<string, string>;
};
function readJson<T>(filePath: string): T {
return JSON.parse(readFileSync(filePath, 'utf8')) as T;
}
function main() {
const cwd = process.cwd();
const baselinePath = resolve(cwd, 'deps-baseline.json');
const packagePath = resolve(cwd, 'package.json');
const contextSchemaPath = resolve(cwd, 'schemas', 'wallet-context.v1.schema.json');
if (!existsSync(baselinePath)) {
console.error(`[deps:check] missing deps-baseline.json at ${baselinePath}`);
process.exit(1);
}
if (!existsSync(packagePath)) {
console.error(`[deps:check] missing package.json at ${packagePath}`);
process.exit(1);
}
const baseline = readJson<Baseline>(baselinePath);
const pkg = readJson<any>(packagePath);
const declaredDeps = {
...(pkg.dependencies || {}),
...(pkg.devDependencies || {}),
} as Record<string, string>;
const failures: string[] = [];
for (const [name, expected] of Object.entries(baseline.dependencies || {})) {
const actual = declaredDeps[name];
if (!actual) {
failures.push(`${name}: missing (expected ${expected})`);
continue;
}
if (actual !== expected) {
failures.push(`${name}: expected ${expected}, got ${actual}`);
}
}
const contextPath =
process.env.PORTKEY_SKILL_WALLET_CONTEXT_PATH ||
resolve(homedir(), '.portkey', 'skill-wallet', 'context.v1.json');
if (!existsSync(contextSchemaPath)) {
failures.push(`missing wallet-context schema: ${contextSchemaPath}`);
} else if (existsSync(contextPath)) {
try {
const schema = readJson<Record<string, unknown>>(contextSchemaPath);
const contextRaw = readJson<Record<string, unknown>>(contextPath);
const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(schema);
if (!validate(contextRaw)) {
const details = (validate.errors || [])
.map((err) => `${err.instancePath || '/'} ${err.message || 'invalid'}`)
.join('; ');
failures.push(`wallet-context schema validation failed (${contextPath}): ${details}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
failures.push(`wallet-context parse/validation failed: ${message}`);
}
}
if (failures.length > 0) {
console.error('[deps:check] check failed:');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[deps:check] passed');
}
main();
#!/usr/bin/env bun
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
type SectionTotals = {
linesFound: number;
linesHit: number;
funcsFound: number;
funcsHit: number;
};
function isSrcFile(sfPath: string): boolean {
const normalized = sfPath.replace(/\\/g, '/');
return normalized.startsWith('src/') || normalized.includes('/src/');
}
function parseLcov(lcovText: string): SectionTotals {
const totals: SectionTotals = {
linesFound: 0,
linesHit: 0,
funcsFound: 0,
funcsHit: 0,
};
let currentFile = '';
for (const rawLine of lcovText.split('\n')) {
const line = rawLine.trim();
if (!line) continue;
if (line.startsWith('SF:')) {
currentFile = line.slice(3);
continue;
}
if (!currentFile || !isSrcFile(currentFile)) {
continue;
}
if (line.startsWith('LF:')) {
totals.linesFound += Number(line.slice(3)) || 0;
continue;
}
if (line.startsWith('LH:')) {
totals.linesHit += Number(line.slice(3)) || 0;
continue;
}
if (line.startsWith('FNF:')) {
totals.funcsFound += Number(line.slice(4)) || 0;
continue;
}
if (line.startsWith('FNH:')) {
totals.funcsHit += Number(line.slice(4)) || 0;
continue;
}
}
return totals;
}
function percent(hit: number, found: number): number {
if (found <= 0) return 0;
return (hit / found) * 100;
}
function main() {
const minLines = Number(process.env.COVERAGE_MIN_LINES || '85');
const minFuncs = Number(process.env.COVERAGE_MIN_FUNCS || '80');
const lcovFile = process.env.COVERAGE_LCOV_FILE || 'coverage/lcov.info';
const lcovPath = resolve(process.cwd(), lcovFile);
if (!existsSync(lcovPath)) {
console.error(`[coverage-gate] lcov file not found: ${lcovPath}`);
process.exit(1);
}
const lcov = readFileSync(lcovPath, 'utf8');
const totals = parseLcov(lcov);
if (totals.linesFound === 0 || totals.funcsFound === 0) {
console.error('[coverage-gate] no src/** lines/functions coverage data found');
process.exit(1);
}
const linePct = percent(totals.linesHit, totals.linesFound);
const funcPct = percent(totals.funcsHit, totals.funcsFound);
const failures: string[] = [];
if (linePct < minLines) {
failures.push(`lines ${linePct.toFixed(2)}% < ${minLines}%`);
}
if (funcPct < minFuncs) {
failures.push(`funcs ${funcPct.toFixed(2)}% < ${minFuncs}%`);
}
if (failures.length) {
console.error(`[coverage-gate] failed: ${failures.join(', ')}`);
process.exit(1);
}
console.log(
`[coverage-gate] passed: lines=${linePct.toFixed(2)}% funcs=${funcPct.toFixed(2)}% (threshold lines>=${minLines} funcs>=${minFuncs})`,
);
}
main();
import { request } from '../../lib/http-client.js';
import { requireField } from '../../lib/errors.js';
import type {
AccountSummary,
AddressAssetItem,
AddressDetailResponse,
AddressTransferItem,
ApiPagedList,
ContractEventItem,
ContractHistoryItem,
ContractSourceResponse,
ContractSummary,
} from '../../lib/api-types.js';
import type {
AccountsInput,
AddressDetailInput,
AddressNftAssetsInput,
AddressTokensInput,
AddressTransfersInput,
ContractEventsInput,
ContractHistoryInput,
ContractsInput,
ContractSourceInput,
ToolResult,
} from '../../lib/types.js';
import { executeTool, resolveAddress, resolveChainId, toPaginationQuery } from './common.js';
export async function getAccounts(input: AccountsInput = {}): Promise<ToolResult<ApiPagedList<AccountSummary>>> {
return executeTool(async (traceId) => {
return request<ApiPagedList<AccountSummary>>({
traceId,
path: '/api/app/address/accounts',
query: {
chainId: resolveChainId(input.chainId),
...toPaginationQuery(input),
},
});
}, 'GET_ACCOUNTS_FAILED');
}
export async function getContracts(input: ContractsInput = {}): Promise<ToolResult<ApiPagedList<ContractSummary>>> {
return executeTool(async (traceId) => {
return request<ApiPagedList<ContractSummary>>({
traceId,
path: '/api/app/address/contracts',
query: {
chainId: resolveChainId(input.chainId),
...toPaginationQuery(input),
},
});
}, 'GET_CONTRACTS_FAILED');
}
export async function getAddressDetail(input: AddressDetailInput): Promise<ToolResult<AddressDetailResponse>> {
return executeTool(async (traceId) => {
requireField(input.address, 'address');
return request<AddressDetailResponse>({
traceId,
path: '/api/app/address/detail',
query: {
chainId: resolveChainId(input.chainId),
address: resolveAddress(input.address),
},
});
}, 'GET_ADDRESS_DETAIL_FAILED');
}
export async function getAddressTokens(input: AddressTokensInput): Promise<ToolResult<ApiPagedList<AddressAssetItem>>> {
return executeTool(async (traceId) => {
requireField(input.address, 'address');
return request<ApiPagedList<AddressAssetItem>>({
traceId,
path: '/api/app/address/tokens',
query: {
chainId: resolveChainId(input.chainId),
address: resolveAddress(input.address),
fuzzySearch: input.fuzzySearch,
...toPaginationQuery(input),
},
});
}, 'GET_ADDRESS_TOKENS_FAILED');
}
export async function getAddressNftAssets(
input: AddressNftAssetsInput,
): Promise<ToolResult<ApiPagedList<AddressAssetItem>>> {
return executeTool(async (traceId) => {
requireField(input.address, 'address');
return request<ApiPagedList<AddressAssetItem>>({
traceId,
path: '/api/app/address/nft-assets',
query: {
chainId: resolveChainId(input.chainId),
address: resolveAddress(input.address),
fuzzySearch: input.fuzzySearch,
...toPaginationQuery(input),
},
});
}, 'GET_ADDRESS_NFT_ASSETS_FAILED');
}
export async function getAddressTransfers(
input: AddressTransfersInput,
): Promise<ToolResult<ApiPagedList<AddressTransferItem>>> {
return executeTool(async (traceId) => {
requireField(input.address, 'address');
return request<ApiPagedList<AddressTransferItem>>({
traceId,
path: '/api/app/address/transfers',
query: {
chainId: resolveChainId(input.chainId),
address: resolveAddress(input.address),
symbol: input.symbol,
tokenType: input.tokenType,
...toPaginationQuery(input),
},
});
}, 'GET_ADDRESS_TRANSFERS_FAILED');
}
export async function getContractHistory(
input: ContractHistoryInput,
): Promise<ToolResult<ApiPagedList<ContractHistoryItem>>> {
return executeTool(async (traceId) => {
requireField(input.address, 'address');
return request<ApiPagedList<ContractHistoryItem>>({
traceId,
path: '/api/app/address/contract/history',
query: {
chainId: resolveChainId(input.chainId),
address: resolveAddress(input.address),
},
});
}, 'GET_CONTRACT_HISTORY_FAILED');
}
export async function getContractEvents(
input: ContractEventsInput,
): Promise<ToolResult<ApiPagedList<ContractEventItem>>> {
return executeTool(async (traceId) => {
requireField(input.contractAddress, 'contractAddress');
return request<ApiPagedList<ContractEventItem>>({
traceId,
path: '/api/app/address/contract/events',
query: {
chainId: resolveChainId(input.chainId),
contractAddress: resolveAddress(input.contractAddress),
blockHeight: input.blockHeight,
...toPaginationQuery(input),
},
});
}, 'GET_CONTRACT_EVENTS_FAILED');
}
export async function getContractSource(input: ContractSourceInput): Promise<ToolResult<ContractSourceResponse>> {
return executeTool(async (traceId) => {
requireField(input.address, 'address');
return request<ContractSourceResponse>({
traceId,
path: '/api/app/address/contract/file',
query: {
chainId: resolveChainId(input.chainId),
address: resolveAddress(input.address),
},
});
}, 'GET_CONTRACT_SOURCE_FAILED');
}
import { request } from '../../lib/http-client.js';
import { requireField, SkillError } from '../../lib/errors.js';
import type {
AddressDictionaryResponse,
BlockDetailResponse,
BlockchainOverviewResponse,
BlockListResponse,
LogEventsResponse,
TransactionDataChartResponse,
TransactionDetailResponse,
TransactionListResponse,
} from '../../lib/api-types.js';
import type {
AddressDictionaryInput,
BlockchainOverviewInput,
BlockDetailInput,
BlocksInput,
LogEventsInput,
ToolResult,
TransactionDataChartInput,
TransactionDetailInput,
TransactionsInput,
} from '../../lib/types.js';
import { executeTool, resolveChainId, toPaginationQuery } from './common.js';
export async function getBlocks(input: BlocksInput = {}): Promise<ToolResult<BlockListResponse>> {
return executeTool(async (traceId) => {
const result = await request<BlockListResponse>({
traceId,
path: '/api/app/blockchain/blocks',
query: {
chainId: resolveChainId(input.chainId),
...toPaginationQuery(input),
isLastPage: input.isLastPage,
},
});
return result;
}, 'GET_BLOCKS_FAILED');
}
export async function getLatestBlocks(input: Omit<BlocksInput, 'skipCount'> = {}): Promise<ToolResult<BlockListResponse>> {
return getBlocks({
...input,
skipCount: 0,
maxResultCount: input.maxResultCount ?? 6,
});
}
export async function getBlockDetail(input: BlockDetailInput): Promise<ToolResult<BlockDetailResponse>> {
return executeTool(async (traceId) => {
requireField(input.blockHeight, 'blockHeight');
const result = await request<BlockDetailResponse>({
traceId,
path: '/api/app/blockchain/blockDetail',
query: {
chainId: resolveChainId(input.chainId),
blockHeight: input.blockHeight,
},
});
return result;
}, 'GET_BLOCK_DETAIL_FAILED');
}
export async function getTransactions(input: TransactionsInput = {}): Promise<ToolResult<TransactionListResponse>> {
return executeTool(async (traceId) => {
const result = await request<TransactionListResponse>({
traceId,
path: '/api/app/blockchain/transactions',
query: {
chainId: resolveChainId(input.chainId),
...toPaginationQuery(input),
transactionId: input.transactionId,
blockHeight: input.blockHeight,
address: input.address,
startTime: input.startTime,
endTime: input.endTime,
},
});
return result;
}, 'GET_TRANSACTIONS_FAILED');
}
export async function getLatestTransactions(
input: Omit<TransactionsInput, 'skipCount'> = {},
): Promise<ToolResult<TransactionListResponse>> {
return getTransactions({
...input,
skipCount: 0,
maxResultCount: input.maxResultCount ?? 6,
});
}
export async function getTransactionDetail(input: TransactionDetailInput): Promise<ToolResult<TransactionDetailResponse>> {
return executeTool(async (traceId) => {
requireField(input.transactionId, 'transactionId');
const result = await request<TransactionDetailResponse>({
traceId,
path: '/api/app/blockchain/transactionDetail',
query: {
chainId: resolveChainId(input.chainId),
transactionId: input.transactionId,
blockHeight: input.blockHeight,
},
});
return result;
}, 'GET_TRANSACTION_DETAIL_FAILED');
}
export async function getBlockchainOverview(
input: BlockchainOverviewInput = {},
): Promise<ToolResult<BlockchainOverviewResponse>> {
return executeTool(async (traceId) => {
const result = await request<BlockchainOverviewResponse>({
traceId,
method: 'POST',
path: '/api/app/blockchain/blockchainOverview',
body: {
...input,
chainId: resolveChainId(input.chainId),
},
});
return result;
}, 'GET_BLOCKCHAIN_OVERVIEW_FAILED');
}
export async function getTransactionDataChart(
input: TransactionDataChartInput = {},
): Promise<ToolResult<TransactionDataChartResponse>> {
return executeTool(async (traceId) => {
const result = await request<TransactionDataChartResponse>({
traceId,
method: 'POST',
path: '/api/app/blockchain/transactionDataChart',
body: {
...input,
chainId: resolveChainId(input.chainId),
},
});
return result;
}, 'GET_TRANSACTION_DATA_CHART_FAILED');
}
export async function getAddressDictionary(input: AddressDictionaryInput): Promise<ToolResult<AddressDictionaryResponse>> {
return executeTool(async (traceId) => {
requireField(input.name, 'name');
const addresses = requireField(input.addresses, 'addresses');
if (!Array.isArray(addresses) || addresses.length === 0) {
throw new SkillError('INVALID_INPUT', 'addresses must be a non-empty array');
}
const result = await request<AddressDictionaryResponse>({
traceId,
method: 'POST',
path: '/api/app/blockchain/addressDictionary',
body: {
...input,
chainId: resolveChainId(input.chainId),
},
});
return result;
}, 'GET_ADDRESS_DICTIONARY_FAILED');
}
export async function getLogEvents(input: LogEventsInput): Promise<ToolResult<LogEventsResponse>> {
return executeTool(async (traceId) => {
requireField(input.contractAddress, 'contractAddress');
const result = await request<LogEventsResponse>({
traceId,
method: 'POST',
path: '/api/app/blockchain/logEvents',
body: {
...input,
chainId: resolveChainId(input.chainId),
contractAddress: input.contractAddress,
...toPaginationQuery(input),
},
});
return result;
}, 'GET_LOG_EVENTS_FAILED');
}
#!/usr/bin/env bun
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import packageJson from '../../package.json';
import { asMcpResult } from './output.js';
import { MCP_TOOL_DESCRIPTORS } from '../tooling/tool-descriptors.js';
const server = new McpServer({
name: 'aelfscan-skill',
version: packageJson.version,
});
for (const descriptor of MCP_TOOL_DESCRIPTORS) {
server.registerTool(
descriptor.mcpName,
{
description: descriptor.description,
inputSchema: descriptor.inputSchema,
},
async (input: Record<string, unknown>) => {
const validatedInput = descriptor.parse(input);
const result = await descriptor.handler(validatedInput);
return asMcpResult(result, descriptor.outputPolicy);
},
);
}
const transport = new StdioServerTransport();
await server.connect(transport);
Related skills
FAQ
Can aelfscan-skill write to the chain?
No, it is read-only and routes any write intent to separate wallet and domain write skills.
What integrations does it support?
SDK, CLI, MCP, and OpenClaw from a single tool descriptor source.