The authoritative architecture reference for this repository. It explains what this package is, how its parts connect, how a tool call flows from an AI agent to the Hashlock Markets backend and back, and the reasoning behind the design — verified against
main(counts reflect the code as of 2026-05-30).This is the publishable, stdio MCP server for Hashlock Markets. For the system it ultimately talks to, read the master doc: hashlock-markets / ARCHITECTURE.md.
Every non-obvious claim points at a
path:lineyou can open. If a number here ever disagrees with the code, the code wins — please fix the doc.
@hashlock-tech/mcp is the canonical Model Context Protocol server for Hashlock Markets,
distributed on npm and runnable with npx. It lets any MCP-compatible client — Claude
Desktop, Cursor, Windsurf, Claude Code — drive Hashlock Markets trading (create RFQs, quote
as a maker, record HTLC locks, claim/refund, query status) through six tools.
The whole server is one file — src/index.ts (206 lines) — that registers 6 tools on
an McpServer and serves them over stdio (src/index.ts:25,198). It holds no business
logic of its own: every tool is a thin adapter that calls a method on the
@hashlock-tech/sdk client (a direct
dependency, package.json:33), which in turn speaks GraphQL to the backend.
AI agent (MCP client) → hashlock-mcp (this, stdio) → @hashlock-tech/sdk → api-gateway /graphql
| Decision | Why |
|---|---|
| Thin adapter over the SDK | The server adds no transport, retry, or schema logic — it imports HashLock (src/index.ts:4,18) and forwards. All network behaviour (retries, error mapping, the /graphql target) lives in the SDK, so the two repos can't drift on protocol. |
| stdio transport | StdioServerTransport (src/index.ts:198) is what desktop MCP clients launch as a subprocess. The bin entry + shebang banner (package.json:15-17, tsup.config.ts:11) make the built dist/index.js directly executable via npx -y @hashlock-tech/mcp. |
The create_rfq description is an LLM prompt, not just docs |
Its 59-line description constant (src/index.ts:99-157) is an intent compiler: it teaches the model to map free-text (EN + Turkish) to structured params, infer chains, and restate-and-confirm before spending real funds. Tests pin its load-bearing keywords (src/__tests__/tools.test.ts:201-279) so a copy-edit can't silently change agent behaviour. |
| Six canonical tools, mirroring the backend | The tool set (create_rfq, respond_rfq, create_htlc, withdraw_htlc, refund_htlc, get_htlc) is the same canonical six the backend mcp-gateway exposes — this package is the downloadable stdio sibling of that production HTTP path (see §8). |
| Secrets only via env | The SIWE JWT is read from HASHLOCK_ACCESS_TOKEN (src/index.ts:16); nothing is hard-coded. The MCP-Registry/Smithery manifests mark it isSecret (mcp-registry.json:36, smithery.yaml:13). |
graph TB
subgraph "MCP clients"
CLIENT["Claude Desktop / Cursor / Windsurf / Claude Code"]
end
subgraph "hashlock-mcp (this repo, npm: @hashlock-tech/mcp)"
SRV["McpServer 'hashlock'<br/>src/index.ts — stdio"]
T1["create_rfq · respond_rfq"]
T2["create_htlc · withdraw_htlc<br/>refund_htlc · get_htlc"]
SRV --> T1 & T2
end
SDK["@hashlock-tech/sdk<br/>(dependency — repo hashlock-sdk)"]
subgraph "Hashlock Markets (separate repo)"
GW["api-gateway :4000 — /graphql"]
MCPGW["mcp-gateway :4011 — /mcp<br/>(remote streamable-http alt path)"]
end
CLIENT -->|"stdio (npx -y)"| SRV
T1 & T2 --> SDK
SDK -->|"HTTP POST + Bearer JWT"| GW
CLIENT -.->|"remote alternative:<br/>streamable-http"| MCPGW
MCPGW --> GW
Reading it: an MCP client launches this server as a stdio subprocess and calls its tools.
Each tool forwards to the SDK, which POSTs to the backend's /graphql with the agent's
Bearer JWT. The dotted edge is the alternative: instead of running this package locally, a
client can connect directly to the remote https://hashlock.markets/mcp endpoint (the
backend's mcp-gateway) — both paths expose the same six tools (README.md:22-54).
git ls-files is 18 files. The code is a single module; the rest is distribution and config:
| File | Role |
|---|---|
src/index.ts |
The entire server: SDK client setup, 6 tool registrations, stdio bootstrap (src/index.ts:1-206). |
src/__tests__/tools.test.ts |
~30 cases — tool→SDK integration, the create_rfq description-keyword pins, experimental-field passthrough, error scenarios. |
package.json |
npm metadata: name @hashlock-tech/mcp, bin: hashlock-mcp, deps on the SDK + MCP SDK + zod (package.json:2,15-17,32-36). |
mcp-registry.json |
Official MCP Registry manifest (io.github.Hashlock-Tech/hashlock): both the npm/stdio package and the remote streamable-http endpoint (mcp-registry.json:13-59). |
smithery.yaml |
Smithery.ai install manifest (stdio via npx, config schema). |
glama.json |
Glama.ai directory pointer (maintainer). |
llms-install.md |
Agent-readable install guide for auto-discovery. |
tsup.config.ts |
Build: ESM-only + .d.ts, shebang banner so the output is executable (tsup.config.ts:5,11). |
.github/workflows/ci.yml, tsconfig.json, vitest.config.ts, CHANGELOG.md, LICENSE, .env.example |
CI, TS config, tests, history, license, env template. |
Each server.tool(name, description, zodSchema, handler) registration validates input with a
Zod schema and forwards to exactly one SDK method:
| Tool | Registered at | Calls (SDK) | Purpose |
|---|---|---|---|
create_rfq |
src/index.ts:159 |
createRFQ |
Open a sealed-bid RFQ (intent-compiled description, §5) |
respond_rfq |
src/index.ts:180 |
submitQuote |
Maker submits a sealed quote |
create_htlc |
src/index.ts:32 |
fundHTLC |
Record an on-chain HTLC lock tx hash |
withdraw_htlc |
src/index.ts:52 |
claimHTLC |
Claim by revealing the preimage |
refund_htlc |
src/index.ts:69 |
refundHTLC |
Refund after timelock expiry |
get_htlc |
src/index.ts:85 |
getHTLCStatus |
Read live HTLC status (both legs) |
Every handler returns the SDK result JSON-stringified into MCP content (e.g.
src/index.ts:45-46). Because the tools delegate to the SDK, the chain-awareness,
multi-chain support (EVM / Bitcoin / Sui via the chainType param), and cross-chain
RFQ semantics are exactly the SDK's — see the
hashlock-sdk architecture doc.
The "record, don't sign" boundary carries over:
create_htlcdoes not broadcast a transaction — it records atxHashthe agent's wallet already broadcast on-chain. The backend's chain-watcher remains the sole authority for trade state (master doc §5.2/§5.6).
The single most distinctive piece of this repo is prose, not code: the description string
passed to create_rfq (src/index.ts:99-157). MCP tool descriptions are read by the model,
so this string is effectively a sub-prompt that turns a user's natural-language request into
the structured create_rfq params. It encodes:
- Verb → side mapping in English and Turkish (
sell/sat → SELL,buy/al → BUY,src/index.ts:114-117). - Chain inference defaults (ETH/USDC/USDT/WBTC/WETH →
ethereum, BTC →bitcoin, SUI →sui) with an explicit rule never to silently testnet-ify an unqualified leg (src/index.ts:119-124). - Safety rails: pass raw decimal amounts (don't pre-convert to wei/sats), reject
USD-denominated sizes, and RESTATE the resolved deal and get confirmation before calling
— "Real funds" (
src/index.ts:126-143). - Worked examples, including a cross-chain
SUI/sui ↔ ETH/sepoliaexemplar (src/index.ts:145-156).
src/__tests__/tools.test.ts:201-279 reads the source back and asserts each rule is still
present — a regression guard that treats the prompt as load-bearing behaviour, not docs.
sequenceDiagram
actor Agent as MCP client (LLM)
participant Srv as hashlock-mcp (stdio)
participant SDK as HashLock (SDK)
participant BE as api-gateway /graphql
Agent->>Srv: tools/call create_rfq { baseToken, quoteToken, side, amount }
Srv->>Srv: Zod-validate input (src/index.ts:162-171)
Srv->>SDK: hl.createRFQ(args) (src/index.ts:173)
SDK->>BE: POST mutation createRFQ + Bearer JWT
BE-->>SDK: { data: { createRFQ } }
SDK-->>Srv: RFQ object
Srv-->>Agent: content: [{ type:'text', text: JSON.stringify(rfq) }]
- Local stdio (this package) —
npx -y @hashlock-tech/mcpwithHASHLOCK_ACCESS_TOKENin env (README.md:40-54,smithery.yaml:6-9). The SDK targets/graphql. - Remote streamable-http — point the client at
https://hashlock.markets/mcpwith anAuthorization: Bearerheader (README.md:22-38,mcp-registry.json:46-59). That endpoint is the backendmcp-gateway(nginx routes/mcp→mcp-gateway:4011, master doc §8.1).
Both paths use a SIWE 7-day JWT from hashlock.markets/sign/login (README.md:62-70).
This server never mints it — it only carries it (env var → SDK accessToken,
src/index.ts:16,20), matching the SDK's "bearer-token consumer, not auth provider" model
(master doc §5.5).
The package is published across four agent-discovery surfaces, all pointing at the same npm artifact:
| Surface | Manifest | Identifier |
|---|---|---|
| npm | package.json |
@hashlock-tech/mcp |
| MCP Registry | mcp-registry.json |
io.github.Hashlock-Tech/hashlock (server 1.2.0, mcp-registry.json:11) |
| Smithery.ai | smithery.yaml |
stdio via npx (slug bsozen-4wm5/hashlock-otc-v1, README.md:12) |
| Glama.ai | glama.json |
maintainer BarisSozen |
README.md:140-145 deprecates two legacy packages (hashlock-mcp-server, langchain-hashlock)
that depended on an intent REST API that "was never shipped" — a useful honest signal about the
project's history.
This package is one of the sibling repos catalogued in the master doc's §3 "repositories and how they connect" and is central to its §8 "agent / MCP surface".
| Connection | Detail |
|---|---|
| Dependency chain | hashlock-mcp → @hashlock-tech/sdk (package.json:33) → api-gateway /graphql. This server inherits the SDK's endpoint, retry, and error behaviour wholesale. |
| Tool contract = mcp-gateway's | The six tools here are the same canonical six the backend mcp-gateway exposes (master §8.1). This package is the local-stdio sibling; mcp-gateway is the remote-HTTP production path. The remote install option literally points at it (https://hashlock.markets/mcp). |
Do NOT conflate with services/remote-mcp |
The master doc warns of two backend MCP servers. This package mirrors mcp-gateway (6 canonical settlement tools), not services/remote-mcp (5 intent tools over an external REST API). Different tools, different backend. |
| Auth model | Consumes the SIWE JWT minted by auth-service (master §5.5). |
| Settlement authority | Tools record on-chain actions; hashlock-markets' chain-watcher decides trade state (master §5.6). |
Known doc drift (flagged, not fixed here):
- Endpoint default contradiction. The code defaults to
https://hashlock.markets/graphql(src/index.ts:15) and the comment above it (src/index.ts:6-14) explains that/api/graphqlis the cookie-only SSR proxy that returns 401 for external MCP clients. YetREADME.md:90,mcp-registry.json:40, andsmithery.yaml:26all document the default ashttps://hashlock.markets/api/graphql— the broken path. The runtime is correct; the manifests are stale. Treatsrc/index.ts:15as truth.- Three version numbers.
package.jsonis0.2.0(:3), the in-codeMcpServerversion literal is'0.1.12'(src/index.ts:27), and the MCP-Registryserver.versionis1.2.0(mcp-registry.json:11). They are not reconciled.- Stale legacy references.
llms-install.mdand.env.example:1still cite the compromised142.93.106.129IP and the wrong package name@hashlock/mcp(the canonical name is@hashlock-tech/mcp,package.json:2).- CI does not run tests.
ci.yml:26-28runsinstall → lint → buildonly; the ~30-casetools.test.tssuite is never executed in CI (it must be run locally withpnpm test).These are documentation/packaging lags, not runtime defects; correcting them is out of scope for this architecture PR.
- Build —
tsupemits ESM-onlydist/index.js(+.d.ts) with a#!/usr/bin/env nodebanner so it runs as a CLI (tsup.config.ts:5,11). - Lint —
pnpm lint=tsc --noEmit(package.json:29). - Test —
vitest runovertools.test.ts; covers each tool→SDK call, the description keyword-pins, experimental-field passthrough, and error mapping.⚠️ Run locally — CI does not (see §8 drift note). - CI —
.github/workflows/ci.yml:install → lint → buildon Node 18, 20, 22 (ci.yml:14,26-28).
| Term | Meaning |
|---|---|
| MCP | Model Context Protocol — the tool-calling protocol AI clients speak |
| stdio transport | the server runs as a subprocess; client and server exchange JSON-RPC over stdin/stdout |
| Tool | one server.tool(name, description, schema, handler) registration |
| Intent compiler | the create_rfq description — an LLM sub-prompt that turns free-text into params |
| HTLC / RFQ / SIWE / preimage | see the hashlock-sdk glossary and master doc §11 |
Read next:
- The whole server —
src/index.ts(start at the tool registrations,:32). - The SDK it wraps — hashlock-sdk / ARCHITECTURE.md.
- The backend it ultimately drives — hashlock-markets / ARCHITECTURE.md.
For the Russian version see ARCHITECTURE.ru.md.