Files
zeekayandHanzo Dev a42aceb5cd feat(mcp): expose the Earn / Liquid Protocol vault tools
@luxfi/bank-mcp 1.1.0 — add six agent tools over the SDK's earn methods:
bank_vaults, bank_earn_vaults, and bank_earn_{deposit,borrow,repay,withdraw},
so an agent can read the vault catalog + the caller's positions and move
collateral/debt. 28 tools total; SDK dep pinned to the published ^1.4.0.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-23 16:01:02 -07:00

81 lines
3.5 KiB
TypeScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { Bank, BankError } from '@luxfi/bank'
import { tools } from '../src/tools.ts'
// A Bank backed by a fake fetch that records the call and returns a canned body.
function mockBank(status = 200, body: unknown = {}) {
const calls: { url: string; method: string; body: unknown }[] = []
const fetchFn = (async (url: string, init: RequestInit = {}) => {
calls.push({ url: String(url), method: init.method ?? 'GET', body: init.body ? JSON.parse(String(init.body)) : undefined })
return { ok: status < 400, status, json: async () => body } as Response
}) as unknown as typeof fetch
return { bank: new Bank({ baseUrl: 'https://api.x', token: 'tok', fetch: fetchFn }), calls }
}
function find(name: string) {
const tool = tools.find((t) => t.name === name)
assert.ok(tool, `tool ${name} exists`)
return tool!
}
test('the full tool set is present and well-formed', () => {
assert.equal(tools.length, 28)
// Earn (Liquid Protocol vaults) tools are present.
for (const n of ['bank_vaults', 'bank_earn_vaults', 'bank_earn_deposit', 'bank_earn_borrow', 'bank_earn_repay', 'bank_earn_withdraw']) {
assert.ok(tools.some((t) => t.name === n), `${n} present`)
}
const names = new Set(tools.map((t) => t.name))
assert.equal(names.size, tools.length, 'names are unique')
for (const tool of tools) {
assert.match(tool.name, /^bank_[a-z_]+$/, `${tool.name} is snake_cased`)
assert.ok(tool.description.length > 10, `${tool.name} has a description`)
assert.equal(typeof tool.run, 'function')
}
})
test('read tools call the right endpoint', async () => {
for (const [name, path] of [
['bank_plans', '/v1/bank/plans'],
['bank_wallet', '/v1/bank/wallet'],
['bank_overview', '/v1/bank/overview'],
['bank_cards', '/v1/bank/cards'],
] as const) {
const { bank, calls } = mockBank()
await find(name).run(bank, {} as never)
assert.equal(calls[0].url, `https://api.x${path}`, name)
assert.equal(calls[0].method, 'GET', name)
}
})
test('bank_transfer maps args to the transfer body', async () => {
const { bank, calls } = mockBank()
await find('bank_transfer').run(bank, {
fromAccountId: 'a', toAccountId: 'b', amount: 5000, currency: 'USD', reference: 'r',
} as never)
assert.equal(calls[0].url, 'https://api.x/v1/bank/transfers')
assert.equal(calls[0].method, 'POST')
assert.deepEqual(calls[0].body, { fromAccountId: 'a', toAccountId: 'b', amount: 5000, currency: 'USD', reference: 'r' })
})
test('bank_send_crypto posts asset/amount/toAddress', async () => {
const { bank, calls } = mockBank(200, { txHash: '0xabc' })
const out = await find('bank_send_crypto').run(bank, { asset: 'ETH', amount: 400000, toAddress: '0xdead' } as never)
assert.deepEqual(calls[0].body, { asset: 'ETH', amount: 400000, toAddress: '0xdead' })
assert.equal((out as { txHash: string }).txHash, '0xabc')
})
test('bank_issue_card returns {card, cvv}', async () => {
const { bank } = mockBank(200, { card: { id: 'c1' }, cvv: '999' })
const out = (await find('bank_issue_card').run(bank, { currency: 'USD' } as never)) as { cvv: string }
assert.equal(out.cvv, '999')
})
test('a BankError propagates (the server turns it into an isError result)', async () => {
const { bank } = mockBank(422, { error: 'insufficient balance' })
await assert.rejects(
() => find('bank_send_crypto').run(bank, { asset: 'ETH', amount: 1, toAddress: '0x' } as never),
(e: unknown) => e instanceof BankError && e.status === 422,
)
})