Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ Illustrative output (addresses, keys, block heights and dataset counts vary; thi
Signing verbatim challenge message with EIP-191...
Submitting SIWE login...
Account Created: true
Session Token: rgs_4f5b...

[3/5] Provisioning API key...
Key ID: k_xxxxxxxxxxxx
Expand Down Expand Up @@ -325,12 +324,26 @@ claude mcp add --transport http blockvectra-docs https://docs.blockvectra.com/mc
```

#### Keyed MCP Tools
To enable tools that execute on-chain queries (`rpc_call`, `data_api_get`), pass the API key in the `-H` option:
RPC availability without a key follows the selected chain's public methods and limits. Calls outside that policy, indexed Data API and account tools require your own API key; keyed calls still have method and usage limits.

```bash
claude mcp add --transport http -H "x-api-key: $BLOCKVECTRA_API_KEY" blockvectra-docs https://docs.blockvectra.com/mcp
Merge this entry into your project's `.mcp.json`, preserving any other servers. If the earlier add command created a local entry with the same name, remove that override or update its configuration instead; local entries take precedence over project configuration:

```json
{
"mcpServers": {
"blockvectra-docs": {
"type": "http",
"url": "https://docs.blockvectra.com/mcp",
"headers": {
"x-api-key": "${BLOCKVECTRA_API_KEY}"
}
}
}
}
```

Set `BLOCKVECTRA_API_KEY` privately before starting Claude Code. Keep the literal variable reference in the file; Claude Code [expands it in the header](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcpjson). Do not put the key in CLI arguments or chat.

### 2. Cursor Configuration

Add the server to `.cursor/mcp.json` or through Cursor Settings:
Expand Down
21 changes: 17 additions & 4 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,6 @@ const result = await collectBalancePages(async (cursor) => {
Signing verbatim challenge message with EIP-191...
Submitting SIWE login...
Account Created: true
Session Token: rgs_4f5b...

[3/5] Provisioning API key...
Key ID: k_xxxxxxxxxxxx
Expand Down Expand Up @@ -314,12 +313,26 @@ claude mcp add --transport http blockvectra-docs https://docs.blockvectra.com/mc
```

#### 带 Key 调用工具
若需使用 `rpc_call` 和 `data_api_get` 两个链上数据工具,通过 `-H` 参数传递 API Key:
免 Key RPC 的可用方法和限额以所选链的公开策略为准。策略之外的调用、索引 Data API 和账户工具需要你自己的 API Key;带 Key 调用仍受方法和用量限制。

```bash
claude mcp add --transport http -H "x-api-key: $BLOCKVECTRA_API_KEY" blockvectra-docs https://docs.blockvectra.com/mcp
将此条目合并到项目的 `.mcp.json`,保留其他已有服务。如果之前的添加命令已创建同名 local 条目,请移除该覆盖项或直接更新其配置;local 条目优先于项目配置:

```json
{
"mcpServers": {
"blockvectra-docs": {
"type": "http",
"url": "https://docs.blockvectra.com/mcp",
"headers": {
"x-api-key": "${BLOCKVECTRA_API_KEY}"
}
}
}
}
```

启动 Claude Code 前私密设置 `BLOCKVECTRA_API_KEY`。文件中只保留字面的变量引用,由 Claude Code [在请求头中展开](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcpjson);不要将 Key 放入 CLI 参数或聊天。

### 2. Cursor 配置

在项目根目录 `.cursor/mcp.json` 或 Cursor 设置中添加配置:
Expand Down
6 changes: 0 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ const API_BASE = "https://api.blockvectra.com/v1";
const WALLET_FILE = path.resolve(".agent-wallet.json");
const KEY_FILE = path.resolve(".agent-key.json");

function mask(value: string): string {
return value.length > 8 ? `${value.slice(0, 8)}...` : value;
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function readRpcQuantity(chain: string, apiKey: string, method: "eth_chainId" | "eth_blockNumber") {
Expand Down Expand Up @@ -51,7 +47,6 @@ async function main() {

if (apiKey) {
console.log("[1-3/5] Found BLOCKVECTRA_API_KEY in environment, skipping wallet & key provisioning.\n");
console.log(` API Key: ${mask(apiKey)}`);
} else {
const { account, source } = loadOrCreateWallet({
walletFile: WALLET_FILE,
Expand Down Expand Up @@ -104,7 +99,6 @@ async function main() {
};
const sessionToken = loginData.session.token;
console.log(` Account Created: ${loginData.account_created}`);
console.log(` Session Token: ${mask(sessionToken)}`);

// 3. Create an API key
console.log("\n[3/5] Provisioning API key...");
Expand Down
36 changes: 30 additions & 6 deletions tests/onboarding-key-reuse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const API_KEY = "rgw_fixture_not_a_real_credential_0123456789";
const SESSION = "rgs_fixture_not_a_real_session_0123456789";
const entry = new URL("../src/index.ts", import.meta.url).href;
const loader = new URL("../node_modules/tsx/dist/loader.mjs", import.meta.url).pathname;
function run(directory: string, options: { apiKey?: string; denySave?: boolean; revokeFails?: boolean; expectedStatus?: number } = {}) {
function run(directory: string, options: { apiKey?: string; createdApiKey?: string; session?: string; denySave?: boolean; revokeFails?: boolean; expectedStatus?: number } = {}) {
const apiKey = options.apiKey ?? options.createdApiKey ?? API_KEY;
const session = options.session ?? SESSION;
const callsFile = path.join(directory, "calls.json");
fs.writeFileSync(callsFile, "[]");
const env = { ...process.env };
Expand All @@ -33,13 +35,13 @@ function run(directory: string, options: { apiKey?: string; denySave?: boolean;
fs.writeFileSync(${JSON.stringify(callsFile)}, JSON.stringify(calls));
let body;
if (url.endsWith('/auth/siwe/challenge')) body = {message: 'offline fixture challenge'};
else if (url.endsWith('/auth/siwe/login')) body = {session: {token: ${JSON.stringify(SESSION)}}, account_created: false};
else if (url.endsWith('/auth/siwe/login')) body = {session: {token: ${JSON.stringify(session)}}, account_created: false};
else if (url.endsWith('/revoke')) return new Response('{}', {status: ${JSON.stringify(options.revokeFails ? 503 : 200)}});
else if (url.endsWith('/keys')) body = {key: {key_id: 'k_fixture', status: 'active'}, api_key: ${JSON.stringify(API_KEY)}};
else if (url.endsWith('/keys')) body = {key: {key_id: 'k_fixture', status: 'active'}, api_key: ${JSON.stringify(apiKey)}};
else if (url.endsWith('/chains')) body = {chains: [{chain: 'robinhood_mainnet', chain_id: 4663}]};
else if (url.endsWith('/status/freshness')) body = {meta: {chain: 'robinhood_mainnet'}, data: []};
else if (url.endsWith('/robinhood_mainnet')) {
if (new Headers(init.headers).get('x-api-key') !== ${JSON.stringify(API_KEY)}) throw new Error('wrong fixture credential');
if (new Headers(init.headers).get('x-api-key') !== ${JSON.stringify(apiKey)}) throw new Error('wrong fixture credential');
const request = JSON.parse(init.body);
body = {jsonrpc: '2.0', id: 1, result: request.method === 'eth_chainId' ? '0x1237' : '0x2a'};
} else throw new Error('unplanned fixture request');
Expand All @@ -48,8 +50,10 @@ function run(directory: string, options: { apiKey?: string; denySave?: boolean;
await import(${JSON.stringify(entry)});
`], {cwd: directory, env, encoding: 'utf8', timeout: 5000});
assert.equal(child.status, options.expectedStatus ?? 0, child.stderr);
assert.ok(!(child.stdout + child.stderr).includes(API_KEY));
assert.ok(!(child.stdout + child.stderr).includes(SESSION));
for (const secret of [apiKey, session]) {
assert.ok(!(child.stdout + child.stderr).includes(secret), 'must not print a credential');
assert.ok(!(child.stdout + child.stderr).includes(secret.slice(0, 8)), 'must not print a credential prefix');
}
const calls = JSON.parse(fs.readFileSync(callsFile, 'utf8')) as Array<{url: string; method: string}>;
return {calls, stdout: child.stdout, stderr: child.stderr};
}
Expand All @@ -69,6 +73,26 @@ test("two complete offline CLI runs create one key and reuse its exact saved byt
assert.ok(fs.readFileSync(keyFile).equals(bytes));
});

for (const [length, apiKey, session] of [
['short', 'qA7!sE', 'tK9!rZ'],
['long', 'qA7!sE_fixture_key_0123456789', 'tK9!rZ_fixture_session_0123456789'],
]) {
for (const fromEnvironment of [false, true]) {
test(`${length} credentials stay out of CLI output (environment key=${fromEnvironment})`, (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'quickstart-no-secret-'));
t.after(() => fs.rmSync(directory, {recursive: true, force: true}));
const {calls, stdout} = run(directory, {
...(fromEnvironment ? {apiKey} : {createdApiKey: apiKey}),
session,
});
assert.equal(calls.length, fromEnvironment ? 4 : 7);
assert.equal(calls.filter(c => c.url.includes('/auth/')).length, fromEnvironment ? 0 : 2);
assert.equal(calls.filter(c => c.url.endsWith('/keys')).length, fromEnvironment ? 0 : 1);
assert.match(stdout, /Quickstart Completed Successfully/);
});
}
}

for (const revokeFails of [false, true]) {
test(`a local save failure attempts exactly one revoke (accepted=${!revokeFails}) and stops before RPC`, (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'quickstart-key-save-'));
Expand Down
Loading