diff --git a/.changeset/modern-rabbits-visit.md b/.changeset/modern-rabbits-visit.md new file mode 100644 index 0000000..3230965 --- /dev/null +++ b/.changeset/modern-rabbits-visit.md @@ -0,0 +1,5 @@ +--- +"@iqai/mcp-upbit": patch +--- + +add new withdraw & deposit tools diff --git a/README.md b/README.md index 73ad242..cc1f554 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,22 @@ UPBIT_ENABLE_TRADING=false Private tools require `UPBIT_ACCESS_KEY`, `UPBIT_SECRET_KEY`, and `UPBIT_ENABLE_TRADING=true`. +### Security & permissions + +- Keep your `UPBIT_SECRET_KEY` private and IP-allowlist your server in Upbit. +- Set `UPBIT_ENABLE_TRADING=true` only when you intend to place/cancel orders or create withdrawals/deposit addresses. +- Upbit permission mapping (high-level): + - Orders: create/cancel → 주문하기, query/list → 주문조회 + - Accounts/balances → 자산조회 + - Withdrawals: create/cancel → 출금하기, query/list/address list → 출금조회 + - Deposits: create deposit address → 입금하기, query/list/chance/address read → 입금조회 + - Auth follows Upbit JWT with `query_hash` (sorted URL-encoded params). POSTs send JSON bodies and are signed based on that body. + - See docs: 인증, 주문 생성, 출금/입금 레퍼런스 + - Auth: [docs.upbit.com/kr/reference/auth](https://docs.upbit.com/kr/reference/auth) + - Orders: [docs.upbit.com/kr/reference/new-order](https://docs.upbit.com/kr/reference/new-order) + - Withdrawals: [list-withdrawal-addresses](https://docs.upbit.com/kr/reference/list-withdrawal-addresses), [withdraw](https://docs.upbit.com/kr/reference/withdraw) + - Deposits: [available-deposit-information](https://docs.upbit.com/kr/reference/available-deposit-information), [create-deposit-address](https://docs.upbit.com/kr/reference/create-deposit-address) + ### Where to get Upbit API keys Before you begin, you need to get your Upbit API keys: @@ -79,9 +95,67 @@ Private (requires env and enable flag): - `GET_ORDER` — `{ uuid? , identifier? }` - `CREATE_ORDER` — `{ market, side, ord_type, volume?, price? }` - `CANCEL_ORDER` — `{ uuid }` +- `LIST_WITHDRAWAL_ADDRESSES` +- `CREATE_WITHDRAWAL` — `{ currency, amount, address, net_type, secondary_address? , transaction_type? }` +- `GET_WITHDRAWAL` — `{ uuid }` +- `LIST_WITHDRAWALS` — `{ currency?, state?, page?, limit? }` +- `CANCEL_WITHDRAWAL` — `{ uuid }` +- `GET_DEPOSIT_CHANCE` — `{ currency, net_type? }` +- `CREATE_DEPOSIT_ADDRESS` — `{ currency, net_type }` +- `GET_DEPOSIT_ADDRESS` — `{ currency, net_type }` +- `LIST_DEPOSIT_ADDRESSES` +- `GET_DEPOSIT` — `{ uuid }` +- `LIST_DEPOSITS` — `{ currency?, state?, page?, limit? }` All tool outputs are JSON strings for easy display. +### Examples + +```json +// CREATE_ORDER (market buy 10,000 KRW) +{ + "name": "CREATE_ORDER", + "arguments": { + "market": "KRW-BTC", + "side": "bid", + "ord_type": "price", + "price": "10000" + } +} +``` + +```json +// CREATE_ORDER (limit sell with post_only) +{ + "name": "CREATE_ORDER", + "arguments": { + "market": "KRW-BTC", + "side": "ask", + "ord_type": "limit", + "volume": "0.01", + "price": "100000000", + "time_in_force": "post_only", + "identifier": "my-unique-id-001" + } +} +``` + +```json +// LIST_WITHDRAWALS (page 1, 50 items) +{ + "name": "LIST_WITHDRAWALS", + "arguments": { "page": 1, "limit": 50 } +} +``` + +```json +// GET_DEPOSIT_CHANCE +{ + "name": "GET_DEPOSIT_CHANCE", + "arguments": { "currency": "BTC" } +} +``` + ## License MIT diff --git a/biome.json b/biome.json index 37afd11..5da2cff 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,15 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", "files": { - "ignore": ["node_modules/**", "dist/**"] + "includes": [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "**/*.json", + "!**/dist" + ], + "ignoreUnknown": false }, "linter": { "enabled": true, @@ -12,7 +20,11 @@ "formatter": { "enabled": true }, - "organizeImports": { - "enabled": true + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } } } diff --git a/package.json b/package.json index e275faa..63639e5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "bin": { "mcp-upbit-server": "dist/index.js" }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsc && shx chmod +x dist/index.js", "prepare": "husky", @@ -39,7 +41,7 @@ "zod": "^3.25.7" }, "devDependencies": { - "@biomejs/biome": "*", + "@biomejs/biome": "^2.2.0", "@changesets/cli": "^2.29.4", "@types/node": "^22.15.19", "husky": "^9.0.0", @@ -49,7 +51,7 @@ }, "lint-staged": { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [ - "biome check --write --organize-imports-enabled=false --no-errors-on-unmatched" + "biome check --write --no-errors-on-unmatched" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdd38f9..eda619e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,8 +34,8 @@ importers: version: 3.25.7 devDependencies: '@biomejs/biome': - specifier: '*' - version: 1.9.4 + specifier: ^2.2.0 + version: 2.2.0 '@changesets/cli': specifier: ^2.29.4 version: 2.29.4 @@ -61,55 +61,55 @@ packages: resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} engines: {node: '>=6.9.0'} - '@biomejs/biome@1.9.4': - resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + '@biomejs/biome@2.2.0': + resolution: {integrity: sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@1.9.4': - resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + '@biomejs/cli-darwin-arm64@2.2.0': + resolution: {integrity: sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@1.9.4': - resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + '@biomejs/cli-darwin-x64@2.2.0': + resolution: {integrity: sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@1.9.4': - resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + '@biomejs/cli-linux-arm64-musl@2.2.0': + resolution: {integrity: sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@1.9.4': - resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + '@biomejs/cli-linux-arm64@2.2.0': + resolution: {integrity: sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@1.9.4': - resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + '@biomejs/cli-linux-x64-musl@2.2.0': + resolution: {integrity: sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@1.9.4': - resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + '@biomejs/cli-linux-x64@2.2.0': + resolution: {integrity: sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@1.9.4': - resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + '@biomejs/cli-win32-arm64@2.2.0': + resolution: {integrity: sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@1.9.4': - resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + '@biomejs/cli-win32-x64@2.2.0': + resolution: {integrity: sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1330,39 +1330,39 @@ snapshots: '@babel/runtime@7.27.1': {} - '@biomejs/biome@1.9.4': + '@biomejs/biome@2.2.0': optionalDependencies: - '@biomejs/cli-darwin-arm64': 1.9.4 - '@biomejs/cli-darwin-x64': 1.9.4 - '@biomejs/cli-linux-arm64': 1.9.4 - '@biomejs/cli-linux-arm64-musl': 1.9.4 - '@biomejs/cli-linux-x64': 1.9.4 - '@biomejs/cli-linux-x64-musl': 1.9.4 - '@biomejs/cli-win32-arm64': 1.9.4 - '@biomejs/cli-win32-x64': 1.9.4 - - '@biomejs/cli-darwin-arm64@1.9.4': + '@biomejs/cli-darwin-arm64': 2.2.0 + '@biomejs/cli-darwin-x64': 2.2.0 + '@biomejs/cli-linux-arm64': 2.2.0 + '@biomejs/cli-linux-arm64-musl': 2.2.0 + '@biomejs/cli-linux-x64': 2.2.0 + '@biomejs/cli-linux-x64-musl': 2.2.0 + '@biomejs/cli-win32-arm64': 2.2.0 + '@biomejs/cli-win32-x64': 2.2.0 + + '@biomejs/cli-darwin-arm64@2.2.0': optional: true - '@biomejs/cli-darwin-x64@1.9.4': + '@biomejs/cli-darwin-x64@2.2.0': optional: true - '@biomejs/cli-linux-arm64-musl@1.9.4': + '@biomejs/cli-linux-arm64-musl@2.2.0': optional: true - '@biomejs/cli-linux-arm64@1.9.4': + '@biomejs/cli-linux-arm64@2.2.0': optional: true - '@biomejs/cli-linux-x64-musl@1.9.4': + '@biomejs/cli-linux-x64-musl@2.2.0': optional: true - '@biomejs/cli-linux-x64@1.9.4': + '@biomejs/cli-linux-x64@2.2.0': optional: true - '@biomejs/cli-win32-arm64@1.9.4': + '@biomejs/cli-win32-arm64@2.2.0': optional: true - '@biomejs/cli-win32-x64@1.9.4': + '@biomejs/cli-win32-x64@2.2.0': optional: true '@changesets/apply-release-plan@7.0.12': diff --git a/src/index.ts b/src/index.ts index 638938e..022c078 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,24 @@ #!/usr/bin/env node import { FastMCP } from "fastmcp"; import { cancelOrderTool } from "./tools/cancel-order.js"; +import { cancelWithdrawalTool } from "./tools/cancel-withdrawal.js"; +import { createDepositAddressTool } from "./tools/create-deposit-address.js"; import { createOrderTool } from "./tools/create-order.js"; +import { createWithdrawalTool } from "./tools/create-withdrawal.js"; import { getAccountsTool } from "./tools/get-accounts.js"; +import { getDepositTool } from "./tools/get-deposit.js"; +import { getDepositAddressTool } from "./tools/get-deposit-address.js"; +import { getDepositChanceTool } from "./tools/get-deposit-chance.js"; import { getOrderTool } from "./tools/get-order.js"; import { getOrderbookTool } from "./tools/get-orderbook.js"; import { getOrdersTool } from "./tools/get-orders.js"; import { getTickerTool } from "./tools/get-ticker.js"; import { getTradesTool } from "./tools/get-trades.js"; +import { getWithdrawalTool } from "./tools/get-withdrawal.js"; +import { listDepositAddressesTool } from "./tools/list-deposit-addresses.js"; +import { listDepositsTool } from "./tools/list-deposits.js"; +import { listWithdrawalAddressesTool } from "./tools/list-withdrawal-addresses.js"; +import { listWithdrawalsTool } from "./tools/list-withdrawals.js"; async function main() { console.log("Initializing Upbit MCP Server..."); @@ -25,6 +36,17 @@ async function main() { server.addTool(getOrdersTool); server.addTool(getOrderTool); server.addTool(cancelOrderTool); + server.addTool(listWithdrawalAddressesTool); + server.addTool(createWithdrawalTool); + server.addTool(getWithdrawalTool); + server.addTool(listWithdrawalsTool); + server.addTool(cancelWithdrawalTool); + server.addTool(getDepositChanceTool); + server.addTool(createDepositAddressTool); + server.addTool(getDepositAddressTool); + server.addTool(listDepositAddressesTool); + server.addTool(getDepositTool); + server.addTool(listDepositsTool); try { await server.start({ diff --git a/src/lib/upbit-auth.ts b/src/lib/upbit-auth.ts index e2c4e31..b95d0df 100644 --- a/src/lib/upbit-auth.ts +++ b/src/lib/upbit-auth.ts @@ -16,21 +16,23 @@ export function ensurePrivateEnabled(): void { } export function signJwtToken( - queryParams?: Record, + params?: Record, ): string { const payload: Record = { access_key: config.upbit.accessKey, nonce: crypto.randomUUID(), }; - if (queryParams && Object.keys(queryParams).length > 0) { - const queryString = Object.entries(queryParams) - .map(([k, v]) => `${k}=${v}`) - .join("&"); - const queryHash = crypto - .createHash("sha512") - .update(queryString) - .digest("hex"); + if (params && Object.keys(params).length > 0) { + const searchParams = new URLSearchParams(); + const sortedKeys = Object.keys(params).sort(); + for (const key of sortedKeys) { + const value = params[key]; + if (value === undefined) continue; + searchParams.append(key, String(value)); + } + const encoded = searchParams.toString(); + const queryHash = crypto.createHash("sha512").update(encoded).digest("hex"); payload.query_hash = queryHash; payload.query_hash_alg = "SHA512"; } diff --git a/src/tools/cancel-withdrawal.ts b/src/tools/cancel-withdrawal.ts new file mode 100644 index 0000000..a950421 --- /dev/null +++ b/src/tools/cancel-withdrawal.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z.object({ uuid: z.string().min(1) }); + +type Params = z.infer; + +export const cancelWithdrawalTool = { + name: "CANCEL_WITHDRAWAL", + description: + "Cancel a digital asset withdrawal by UUID (requires private API)", + parameters: paramsSchema, + execute: async ({ uuid }: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const query = { uuid }; + const token = signJwtToken(query); + const data = await fetchJson(client, "/withdraw", { + method: "DELETE", + params: query, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/create-deposit-address.ts b/src/tools/create-deposit-address.ts new file mode 100644 index 0000000..879cdf5 --- /dev/null +++ b/src/tools/create-deposit-address.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z + .object({ + currency: z.string(), + net_type: z.string(), + }) + .strict(); + +type Params = z.infer; + +export const createDepositAddressTool = { + name: "CREATE_DEPOSIT_ADDRESS", + description: "Request creation of a deposit address (requires private API)", + parameters: paramsSchema, + execute: async (params: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const token = signJwtToken(params); + const data = await fetchJson(client, "/deposits/coin_address", { + method: "POST", + data: params, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/create-order.ts b/src/tools/create-order.ts index eb503b1..89bae9b 100644 --- a/src/tools/create-order.ts +++ b/src/tools/create-order.ts @@ -10,6 +10,9 @@ const paramsSchema = z ord_type: z.enum(["limit", "price", "market"]), volume: z.string().optional(), price: z.string().optional(), + time_in_force: z.enum(["ioc", "fok", "post_only"]).optional(), + smp_type: z.enum(["cancel_maker", "cancel_taker", "reduce"]).optional(), + identifier: z.string().optional(), }) .refine((p) => (p.ord_type === "limit" ? p.volume && p.price : true), { message: "Limit orders require both volume and price", @@ -19,6 +22,9 @@ const paramsSchema = z }) .refine((p) => (p.ord_type === "market" ? !!p.volume : true), { message: "Market sell (market) requires volume", + }) + .refine((p) => !(p.time_in_force === "post_only" && p.smp_type), { + message: "post_only cannot be used with smp_type", }); type Params = z.infer; @@ -27,17 +33,33 @@ export const createOrderTool = { name: "CREATE_ORDER", description: "Create an Upbit order (requires private API)", parameters: paramsSchema, - execute: async ({ market, side, ord_type, volume, price }: Params) => { + execute: async ({ + market, + side, + ord_type, + volume, + price, + time_in_force, + smp_type, + identifier, + }: Params) => { ensurePrivateEnabled(); const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; const client = createHttpClient(baseURL); - const query: Record = { market, side, ord_type }; - if (volume) query.volume = volume; - if (price) query.price = price; - const token = signJwtToken(query); + const body = { + market, + side, + ord_type, + volume, + price, + time_in_force, + smp_type, + identifier, + }; + const token = signJwtToken(body); const data = await fetchJson(client, "/orders", { method: "POST", - params: query, + data: body, headers: { Authorization: `Bearer ${token}` }, }); return JSON.stringify(data, null, 2); diff --git a/src/tools/create-withdrawal.ts b/src/tools/create-withdrawal.ts new file mode 100644 index 0000000..881d48d --- /dev/null +++ b/src/tools/create-withdrawal.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z + .object({ + currency: z.string(), + amount: z.string(), + address: z.string(), + net_type: z.string(), + secondary_address: z.string().optional(), + transaction_type: z.enum(["default", "internal"]).optional(), + }) + .strict(); + +type Params = z.infer; + +export const createWithdrawalTool = { + name: "CREATE_WITHDRAWAL", + description: "Request a digital asset withdrawal (requires private API)", + parameters: paramsSchema, + execute: async (params: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const token = signJwtToken(params); + const data = await fetchJson(client, "/withdraws/coin", { + method: "POST", + data: params, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/get-deposit-address.ts b/src/tools/get-deposit-address.ts new file mode 100644 index 0000000..a8f1bd9 --- /dev/null +++ b/src/tools/get-deposit-address.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z + .object({ + currency: z.string(), + net_type: z.string(), + }) + .strict(); + +type Params = z.infer; + +export const getDepositAddressTool = { + name: "GET_DEPOSIT_ADDRESS", + description: + "Get a single deposit address for a currency and net_type (private)", + parameters: paramsSchema, + execute: async ({ currency, net_type }: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const query = { currency, net_type }; + const token = signJwtToken(query); + const data = await fetchJson(client, "/deposits/coin_address", { + params: query, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/get-deposit-chance.ts b/src/tools/get-deposit-chance.ts new file mode 100644 index 0000000..007bb9c --- /dev/null +++ b/src/tools/get-deposit-chance.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z + .object({ + currency: z.string(), + net_type: z.string().optional(), + }) + .strict(); + +type Params = z.infer; + +export const getDepositChanceTool = { + name: "GET_DEPOSIT_CHANCE", + description: "Get deposit availability information for a currency (private)", + parameters: paramsSchema, + execute: async ({ currency, net_type }: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const query = { currency, net_type }; + const token = signJwtToken(query); + const data = await fetchJson(client, "/deposits/chance/coin", { + params: query, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/get-deposit.ts b/src/tools/get-deposit.ts new file mode 100644 index 0000000..43161c1 --- /dev/null +++ b/src/tools/get-deposit.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z.object({ uuid: z.string().min(1) }); + +type Params = z.infer; + +export const getDepositTool = { + name: "GET_DEPOSIT", + description: "Get a single deposit by UUID (requires private API)", + parameters: paramsSchema, + execute: async ({ uuid }: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const query = { uuid }; + const token = signJwtToken(query); + const data = await fetchJson(client, "/deposit", { + params: query, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/get-orderbook.ts b/src/tools/get-orderbook.ts index 7ad7a55..1d4b566 100644 --- a/src/tools/get-orderbook.ts +++ b/src/tools/get-orderbook.ts @@ -18,7 +18,7 @@ export const getOrderbookTool = { const data = await fetchJson(client, "/orderbook", { params: { markets: market }, }); - // biome-ignore lint/suspicious/noExplicitAny: + /** biome-ignore lint/suspicious/noExplicitAny: */ const item = Array.isArray(data) ? (data as any[])[0] : (data as any); return JSON.stringify(item, null, 2); }, diff --git a/src/tools/get-ticker.ts b/src/tools/get-ticker.ts index 657aab4..ae42211 100644 --- a/src/tools/get-ticker.ts +++ b/src/tools/get-ticker.ts @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: */ import { z } from "zod"; import { config } from "../lib/config.js"; import { createHttpClient, fetchJson } from "../lib/http.js"; @@ -18,7 +19,6 @@ export const getTickerTool = { const data = await fetchJson(client, "/ticker", { params: { markets: market }, }); - // biome-ignore lint/suspicious/noExplicitAny: const item = Array.isArray(data) ? (data as any[])[0] : (data as any); return JSON.stringify(item, null, 2); }, diff --git a/src/tools/get-withdrawal.ts b/src/tools/get-withdrawal.ts new file mode 100644 index 0000000..beed037 --- /dev/null +++ b/src/tools/get-withdrawal.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z.object({ uuid: z.string().min(1) }); + +type Params = z.infer; + +export const getWithdrawalTool = { + name: "GET_WITHDRAWAL", + description: "Get a single withdrawal by UUID (requires private API)", + parameters: paramsSchema, + execute: async ({ uuid }: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const query = { uuid }; + const token = signJwtToken(query); + const data = await fetchJson(client, "/withdraw", { + params: query, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/list-deposit-addresses.ts b/src/tools/list-deposit-addresses.ts new file mode 100644 index 0000000..09687ab --- /dev/null +++ b/src/tools/list-deposit-addresses.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +export const listDepositAddressesTool = { + name: "LIST_DEPOSIT_ADDRESSES", + description: + "List deposit addresses for all currencies (requires private API)", + parameters: z.object({}), + execute: async () => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const token = signJwtToken(); + const data = await fetchJson(client, "/deposits/coin_addresses", { + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/list-deposits.ts b/src/tools/list-deposits.ts new file mode 100644 index 0000000..1ed4f56 --- /dev/null +++ b/src/tools/list-deposits.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z + .object({ + currency: z.string().optional(), + state: z.string().optional(), + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(50), + }) + .strict(); + +type Params = z.infer; + +export const listDepositsTool = { + name: "LIST_DEPOSITS", + description: "List deposits (requires private API)", + parameters: paramsSchema, + execute: async ({ currency, state, page, limit }: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const query = { + page, + limit, + currency, + state, + }; + const token = signJwtToken(query); + const data = await fetchJson(client, "/deposits", { + params: query, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/list-withdrawal-addresses.ts b/src/tools/list-withdrawal-addresses.ts new file mode 100644 index 0000000..b9b9547 --- /dev/null +++ b/src/tools/list-withdrawal-addresses.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +export const listWithdrawalAddressesTool = { + name: "LIST_WITHDRAWAL_ADDRESSES", + description: + "List registered withdrawal-allowed addresses (requires private API)", + parameters: z.object({}), + execute: async () => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const token = signJwtToken(); + const data = await fetchJson(client, "/withdraws/coin_addresses", { + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const; diff --git a/src/tools/list-withdrawals.ts b/src/tools/list-withdrawals.ts new file mode 100644 index 0000000..96f098b --- /dev/null +++ b/src/tools/list-withdrawals.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; +import { config } from "../lib/config.js"; +import { createHttpClient, fetchJson } from "../lib/http.js"; +import { ensurePrivateEnabled, signJwtToken } from "../lib/upbit-auth.js"; + +const paramsSchema = z + .object({ + currency: z.string().optional(), + state: z.string().optional(), + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(50), + }) + .strict(); + +type Params = z.infer; + +export const listWithdrawalsTool = { + name: "LIST_WITHDRAWALS", + description: "List withdrawals (requires private API)", + parameters: paramsSchema, + execute: async ({ currency, state, page, limit }: Params) => { + ensurePrivateEnabled(); + const baseURL = `${config.upbit.baseUrl}${config.upbit.apiBasePath}`; + const client = createHttpClient(baseURL); + const query = { + page, + limit, + currency, + state, + }; + const token = signJwtToken(query); + const data = await fetchJson(client, "/withdraws", { + params: query, + headers: { Authorization: `Bearer ${token}` }, + }); + return JSON.stringify(data, null, 2); + }, +} as const;