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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist/
.env
coverage/
*.tgz
mcp-publisher.exe
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @hashlock-tech/mcp

> **Hashlock Markets** — trustless, non-custodial OTC trading protocol with zero slippage and DVP (delivery-vs-payment) guarantee. Swap any asset — crypto, RWAs, stablecoins — with sealed-bid RFQ price discovery and HTLC atomic settlement across Ethereum, Bitcoin, and SUI. Agent-friendly via MCP.
> **Hashlock Markets** — the atomic settlement layer for the agent economy. HTLC-based atomic settlement: live on Ethereum and Sui mainnets, with Bitcoin mainnet-ready via P2WSH HTLC scripts (no contract to deploy; signet-validated). No bridges, no custodians, no trust assumptions. Sealed-bid RFQ + HTLC fused into one atomic operation. The settlement primitive AI agents use to trade across chains. MCP-native (6 tools).
>
> **Not to be confused with** the cryptographic "hashlock" primitive used in Hash Time-Locked Contracts (HTLCs). This package is the MCP server for the Hashlock Markets *trading protocol and product* at [hashlock.markets](https://hashlock.markets).
>
Expand All @@ -13,7 +13,7 @@

## What is this?

`@hashlock-tech/mcp` is the canonical [Model Context Protocol](https://modelcontextprotocol.io) server for **Hashlock Markets** — trustless settlement infrastructure for the autonomous economy. It lets AI agents (Claude, GPT, Cursor, Windsurf, any MCP-compatible client) create RFQs, respond as a market maker, fund HTLCs, and settle cross-chain atomic swaps across Ethereum, Bitcoin, and Sui (expanding to Base, Arbitrum, Solana, TON).
`@hashlock-tech/mcp` is the canonical [Model Context Protocol](https://modelcontextprotocol.io) server for **Hashlock Markets** — the atomic settlement layer for the agent economy. It lets AI agents (Claude, GPT, Cursor, Windsurf, any MCP-compatible client) create RFQs, respond as a market maker, fund HTLCs, and settle cross-chain atomic swaps on Ethereum and Sui mainnets, with Bitcoin mainnet-ready via P2WSH HTLC scripts (no contract to deploy; signet-validated). Expanding to Base, Arbitrum, Solana, TON. No bridges, no custodians, no trust assumptions.

Hashlock Markets features 5 industry-first primitives: BTC Collateral Vaults (Sui-native via Hashi), Forward OTC Settlement (T+24h/T+48h), Verified Counterparty Directory, Multi-leg Trade Atomicity, and Execution Rewards with Tiered KYC. Three interaction modes: AI ↔ AI, AI ↔ Human, Human ↔ Human.

Expand Down
2 changes: 1 addition & 1 deletion llms-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## What is this?

HashLock MCP is a Model Context Protocol server that gives AI agents access to institutional OTC crypto trading with atomic settlement (HTLCs). You can create trades, lock assets, and settle trustlessly across Ethereum and Bitcoin.
HashLock MCP is a Model Context Protocol server that gives AI agents access to cross-chain atomic settlement (HTLCs). You can create trades, lock assets, and settle trustlessly across Ethereum and Bitcoin.

## Quick Install

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 21 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "@hashlock-tech/mcp",
"version": "0.1.12",
"version": "0.2.0",
"mcpName": "io.github.Hashlock-Tech/hashlock",
"description": "Hashlock Markets — trustless settlement infrastructure for the autonomous economy. Sealed-bid RFQ + HTLC atomic settlement across Ethereum, Bitcoin, Sui (+Base, Arbitrum, Solana, TON). 5 industry-first primitives. AI agent-native via MCP. NOT the cryptographic HTLC primitive. NOT affiliated with Hashlock Pty Ltd (hashlock.com).",
"description": "Hashlock Markets — atomic settlement layer for the agent economy. Sealed-bid RFQ + HTLC atomic settlement, fused into one operation. Live on Ethereum and Sui mainnets; Bitcoin HTLC mainnet-ready via P2WSH scripts (no contract to deploy; signet-validated); Base, Arbitrum, Solana, TON on the roadmap. The settlement primitive AI agents use to trade across chains. MCP-native, retry-safe, signed receipts. NOT the cryptographic HTLC primitive. NOT affiliated with Hashlock Pty Ltd (hashlock.com).",
"license": "MIT",
"homepage": "https://hashlock.markets",
"author": {
Expand Down Expand Up @@ -30,7 +30,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@hashlock-tech/sdk": "^0.2.0",
"@hashlock-tech/sdk": "^0.1.4",
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^3.23.0"
},
Expand All @@ -41,35 +41,37 @@
"vitest": "^4.1.4"
},
"keywords": [
"agent-economy",
"agent-native",
"ai-agent",
"autonomous-trading",
"mcp",
"mcp-server",
"model-context-protocol",
"atomic-settlement",
"trade-and-settle",
"htlc",
"atomic-swap",
"cross-chain",
"trustless-settlement",
"non-custodial",
"sealed-bid",
"rfq",
"dvp",
"delivery-vs-payment",
"no-counterparty-risk",
"hashlock",
"hashlock-markets",
"htlc",
"otc",
"claude",
"ai-agent",
"atomic-swap",
"defi",
"sealed-bid",
"intent-protocol",
"crypto-trading",
"rfq",
"cross-chain",
"otc",
"ethereum",
"bitcoin",
"sui",
"stablecoin",
"rwa",
"trustless-settlement",
"non-custodial",
"zero-slippage",
"dvp",
"delivery-vs-payment",
"no-counterparty-risk",
"autonomous-economy",
"agent-friendly"
"rwa"
],
"repository": {
"type": "git",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
"name": "io.github.Hashlock-Tech/hashlock",
"version": "0.2.0",
"description": "Trustless cross-chain OTC settlement for AI agents. Sealed-bid RFQ + HTLC atomic swap.",
"vendor": "Hashlock-Tech",
"sourceUrl": "https://github.com/Hashlock-Tech/hashlock-mcp",
"homepage": "https://hashlock.markets",
"license": "MIT",
"runtime": "node",
"transport": ["stdio"],
"features": {
"tools": true,
"resources": false,
"prompts": false
},
"tools": [
{ "name": "create_rfq", "description": "Create a Request for Quote to buy or sell crypto" },
{ "name": "respond_rfq", "description": "Submit a price quote for an open RFQ" },
{ "name": "create_htlc", "description": "Create and fund an HTLC for atomic OTC settlement" },
{ "name": "withdraw_htlc", "description": "Claim an HTLC by revealing the preimage" },
{ "name": "refund_htlc", "description": "Refund an HTLC after timelock expiry" },
{ "name": "get_htlc", "description": "Query HTLC status for a trade" }
],
"installation": {
"npm": "@hashlock-tech/mcp",
"command": "npx -y @hashlock-tech/mcp",
"env": {
"HASHLOCK_ACCESS_TOKEN": {
"description": "7-day SIWE JWT from hashlock.markets/sign/login",
"required": true
}
}
}
}
30 changes: 19 additions & 11 deletions smithery.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Smithery.ai config for HashLock OTC MCP server.
# Smithery.ai config for the Hashlock MCP server.
# Published: https://www.npmjs.com/package/@hashlock-tech/mcp
# Source: https://github.com/Hashlock-Tech/hashlock-mcp
# Registry: io.github.Hashlock-Tech/hashlock
Expand All @@ -14,38 +14,46 @@ config:
properties:
HASHLOCK_ACCESS_TOKEN:
type: "string"
title: "HashLock access token"
title: "Hashlock access token"
description: >-
Bearer token obtained from https://hashlock.markets/sign/login.
Issued via SIWE (Sign-In with Ethereum); valid for 7 days.
Re-sign after expiry.
HASHLOCK_ENDPOINT:
type: "string"
title: "GraphQL endpoint"
description: "Override the HashLock GraphQL endpoint. Default is production."
description: "Override the Hashlock GraphQL endpoint. Default is production."
default: "https://hashlock.markets/api/graphql"

metadata:
name: "Hashlock Markets"
description: >-
Trustless cross-chain OTC settlement for AI agents. Sealed-bid RFQ +
HTLC atomic swap. Zero slippage, zero counterparty risk, non-custodial.
DVP guarantee. ETH/BTC/SUI. Agent-friendly MCP interface for autonomous
trading. 6 tools: create_rfq, respond_rfq, create_htlc, withdraw_htlc,
refund_htlc, get_htlc.
Atomic settlement layer for the agent economy. Sealed-bid RFQ +
HTLC atomic swap, fused into one operation. Two parties on different
chains — autonomous agents or institutional OTC desks — exchange
value without trusting each other or a third party. Zero slippage,
zero counterparty risk, non-custodial. BIS DvP-1 by construction.
ETH/BTC/SUI. MCP-native, retry-safe, signed receipts. 6 tools:
create_rfq, respond_rfq, create_htlc, withdraw_htlc, refund_htlc,
get_htlc.
homepage: "https://hashlock.markets"
repository: "https://github.com/Hashlock-Tech/hashlock-mcp"
license: "MIT"
categories:
- "finance"
- "defi"
- "agents"
- "trading"
tags:
- "agent-economy"
- "agent-native"
- "atomic-settlement"
- "htlc"
- "atomic-swap"
- "otc"
- "cross-chain"
- "rfq"
- "dvp"
- "ethereum"
- "bitcoin"
- "sui"
- "cross-chain"
- "rfq"
- "otc"
101 changes: 101 additions & 0 deletions src/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, it, expect } from 'vitest';
import { classifyError, wrapTool } from '../lib/errors.js';

describe('classifyError', () => {
it('maps GraphQL field-validation / not-found language to TRADE_NOT_FOUND', () => {
const c = classifyError(new Error('No trade found for tradeId xyz'));
expect(c.code).toBe('TRADE_NOT_FOUND');
expect(c.is_retryable).toBe(false);
expect(c.recovery_hint).toMatch(/list_my_trades|verify the tradeId/i);
});

it('maps Unauthorized to UNAUTHORIZED', () => {
expect(classifyError(new Error('Unauthorized – missing api-token')).code).toBe('UNAUTHORIZED');
});

it('maps HTTP 429 / rate language to RATE_LIMITED (retryable)', () => {
const c = classifyError(new Error('Request failed: 429 Too Many Requests'));
expect(c.code).toBe('RATE_LIMITED');
expect(c.is_retryable).toBe(true);
});

it('maps 5xx / rpc language to UPSTREAM_RPC_ERROR (retryable)', () => {
const c = classifyError(new Error('Request failed: 502 Bad Gateway'));
expect(c.code).toBe('UPSTREAM_RPC_ERROR');
expect(c.is_retryable).toBe(true);
});

it('maps expired RFQ language to RFQ_EXPIRED', () => {
expect(classifyError(new Error('RFQ has expired')).code).toBe('RFQ_EXPIRED');
});

it('falls back to UNKNOWN without masking the original message', () => {
const c = classifyError(new Error('totally novel boom'));
expect(c.code).toBe('UNKNOWN');
expect(c.is_retryable).toBe(false);
});

// Issue 1 regression tests: bare /50\d/ and bare /network/ false-positives
it('does NOT classify "amount 500 invalid" as UPSTREAM — bare 50x must not match validation messages', () => {
const c = classifyError(new Error('amount 500 invalid'));
expect(c.code).toBe('VALIDATION_ERROR');
expect(c.is_retryable).toBe(false);
});

it('does NOT classify "unsupported network" as UPSTREAM — bare /network/ must not match validation phrases', () => {
const c = classifyError(new Error('unsupported network'));
expect(c.code).toBe('VALIDATION_ERROR');
});

it('still classifies transient "network request failed" as UPSTREAM_RPC_ERROR (retryable)', () => {
const c = classifyError(new Error('network request failed'));
expect(c.code).toBe('UPSTREAM_RPC_ERROR');
expect(c.is_retryable).toBe(true);
});

it('still classifies "502 Bad Gateway" as UPSTREAM_RPC_ERROR (retryable) — regression guard', () => {
const c = classifyError(new Error('Request failed: 502 Bad Gateway'));
expect(c.code).toBe('UPSTREAM_RPC_ERROR');
expect(c.is_retryable).toBe(true);
});
});

describe('wrapTool', () => {
it('passes through a successful handler result unchanged', async () => {
const wrapped = wrapTool(async (x: number) => ({ content: [{ type: 'text', text: String(x) }] }));
expect(await wrapped(5)).toEqual({ content: [{ type: 'text', text: '5' }] });
});

it('converts a thrown error into a structured envelope as tool content', async () => {
const wrapped = wrapTool(async () => { throw new Error('No trade found for tradeId zzz'); });
const out = await wrapped();
const payload = JSON.parse(out.content[0].text);
expect(payload.error.code).toBe('TRADE_NOT_FOUND');
expect(payload.error.is_retryable).toBe(false);
expect(typeof payload.error.recovery_hint).toBe('string');
expect(payload.error.message).toContain('No trade found');
});

it('converts a non-Error throw (plain string) into a structured envelope', async () => {
const wrapped = wrapTool(async () => { throw 'plain string boom'; });
const out = await wrapped();
const payload = JSON.parse(out.content[0].text);
expect(payload.error.message).toContain('plain string boom');
expect(payload.error.code).toBe('UNKNOWN');
});
});

describe('non-Error object throwables', () => {
it('classifyError extracts .message from a plain object (not [object Object])', () => {
const c = classifyError({ message: 'No trade found for tradeId q' });
expect(c.code).toBe('TRADE_NOT_FOUND');
});

it('wrapTool preserves .message from a thrown plain object', async () => {
const wrapped = wrapTool(async () => { throw { message: 'custom object failure' }; });
const out = await wrapped();
const payload = JSON.parse(out.content[0].text);
expect(payload.error.message).toBe('custom object failure');
expect(payload.error.code).toBe('UNKNOWN');
});
});
Loading
Loading