@pionex/pionex-trade-mcp 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +694 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -4
- package/src/index.ts +78 -35
- package/src/server.ts +149 -0
- package/dist/run-server.js +0 -462
- package/dist/run-server.js.map +0 -1
- package/src/run-server.ts +0 -31
- package/src/tools/account/index.ts +0 -34
- package/src/tools/common/client.ts +0 -117
- package/src/tools/common/utils.ts +0 -19
- package/src/tools/market/index.ts +0 -157
- package/src/tools/orders/index.ts +0 -220
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
7
7
|
// ../core/dist/index.js
|
|
8
8
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
@@ -695,6 +695,7 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|
|
695
695
|
}
|
|
696
696
|
|
|
697
697
|
// ../core/dist/index.js
|
|
698
|
+
import crypto from "crypto";
|
|
698
699
|
function configFilePath() {
|
|
699
700
|
return join(homedir(), ".pionex", "config.toml");
|
|
700
701
|
}
|
|
@@ -718,43 +719,707 @@ var CLIENT_NAMES = {
|
|
|
718
719
|
openclaw: "OpenClaw (mcporter)"
|
|
719
720
|
};
|
|
720
721
|
var SUPPORTED_CLIENTS = Object.keys(CLIENT_NAMES);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
var
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
722
|
+
var PIONEX_API_DEFAULT_BASE_URL = "https://api.pionex.com";
|
|
723
|
+
var MODULES = ["market", "account", "orders"];
|
|
724
|
+
var DEFAULT_MODULES = ["market", "account", "orders"];
|
|
725
|
+
var ConfigError = class extends Error {
|
|
726
|
+
suggestion;
|
|
727
|
+
constructor(message, suggestion) {
|
|
728
|
+
super(message);
|
|
729
|
+
this.name = "ConfigError";
|
|
730
|
+
this.suggestion = suggestion;
|
|
727
731
|
}
|
|
728
|
-
|
|
729
|
-
|
|
732
|
+
};
|
|
733
|
+
var PionexApiError = class extends Error {
|
|
734
|
+
status;
|
|
735
|
+
endpoint;
|
|
736
|
+
responseText;
|
|
737
|
+
constructor(message, opts) {
|
|
738
|
+
super(message);
|
|
739
|
+
this.name = "PionexApiError";
|
|
740
|
+
this.status = opts?.status;
|
|
741
|
+
this.endpoint = opts?.endpoint;
|
|
742
|
+
this.responseText = opts?.responseText;
|
|
730
743
|
}
|
|
731
|
-
|
|
732
|
-
|
|
744
|
+
};
|
|
745
|
+
function toToolErrorPayload(error) {
|
|
746
|
+
if (error instanceof ConfigError) {
|
|
747
|
+
return {
|
|
748
|
+
error: true,
|
|
749
|
+
type: "ConfigError",
|
|
750
|
+
message: error.message,
|
|
751
|
+
suggestion: error.suggestion
|
|
752
|
+
};
|
|
733
753
|
}
|
|
754
|
+
if (error instanceof PionexApiError) {
|
|
755
|
+
return {
|
|
756
|
+
error: true,
|
|
757
|
+
type: "PionexApiError",
|
|
758
|
+
message: error.message,
|
|
759
|
+
status: error.status,
|
|
760
|
+
endpoint: error.endpoint,
|
|
761
|
+
responseText: error.responseText
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
765
|
+
return { error: true, type: "Error", message };
|
|
766
|
+
}
|
|
767
|
+
function parseModuleList(rawModules) {
|
|
768
|
+
if (!rawModules || rawModules.trim().length === 0) return [...DEFAULT_MODULES];
|
|
769
|
+
const trimmed = rawModules.trim().toLowerCase();
|
|
770
|
+
if (trimmed === "all") return [...MODULES];
|
|
771
|
+
const requested = trimmed.split(",").map((x) => x.trim()).filter(Boolean);
|
|
772
|
+
if (requested.length === 0) return [...DEFAULT_MODULES];
|
|
773
|
+
const out = [];
|
|
774
|
+
for (const m of requested) {
|
|
775
|
+
if (!MODULES.includes(m)) {
|
|
776
|
+
throw new ConfigError(`Unknown module "${m}".`, `Use one of: ${MODULES.join(", ")} or "all".`);
|
|
777
|
+
}
|
|
778
|
+
out.push(m);
|
|
779
|
+
}
|
|
780
|
+
return Array.from(new Set(out));
|
|
781
|
+
}
|
|
782
|
+
function loadConfig(cli) {
|
|
783
|
+
const toml = readTomlProfile(cli.profile);
|
|
784
|
+
const apiKey = process.env.PIONEX_API_KEY?.trim() || toml.api_key;
|
|
785
|
+
const apiSecret = process.env.PIONEX_API_SECRET?.trim() || toml.secret_key;
|
|
786
|
+
const hasAuth = Boolean(apiKey && apiSecret);
|
|
787
|
+
const partialAuth = Boolean(apiKey) || Boolean(apiSecret);
|
|
788
|
+
if (partialAuth && !hasAuth) {
|
|
789
|
+
throw new ConfigError(
|
|
790
|
+
"Partial Pionex API credentials detected.",
|
|
791
|
+
"Set both PIONEX_API_KEY and PIONEX_API_SECRET (env vars or config.toml profile)."
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const baseUrl = (cli.baseUrl?.trim() || process.env.PIONEX_BASE_URL?.trim() || toml.base_url || PIONEX_API_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
795
|
+
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
|
796
|
+
throw new ConfigError(`Invalid base URL "${baseUrl}".`, "PIONEX_BASE_URL must start with http:// or https://");
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
apiKey,
|
|
800
|
+
apiSecret,
|
|
801
|
+
hasAuth,
|
|
802
|
+
baseUrl,
|
|
803
|
+
modules: parseModuleList(cli.modules),
|
|
804
|
+
readOnly: cli.readOnly
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function requireAuth(config) {
|
|
808
|
+
if (!config.apiKey || !config.apiSecret) {
|
|
809
|
+
throw new ConfigError(
|
|
810
|
+
"This operation requires authentication, but no Pionex API credentials were found.",
|
|
811
|
+
"Run 'pionex-ai-kit onboard' to create ~/.pionex/config.toml, or set PIONEX_API_KEY and PIONEX_API_SECRET."
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return { apiKey: config.apiKey, apiSecret: config.apiSecret };
|
|
815
|
+
}
|
|
816
|
+
function buildQueryString(query) {
|
|
817
|
+
if (!query) return "";
|
|
818
|
+
const entries = Object.entries(query).filter(([, v]) => v !== void 0 && v !== null);
|
|
819
|
+
if (entries.length === 0) return "";
|
|
820
|
+
const params = new URLSearchParams();
|
|
821
|
+
for (const [k, v] of entries) params.set(k, String(v));
|
|
822
|
+
return params.toString();
|
|
823
|
+
}
|
|
824
|
+
function buildSignedRequest(config, method, path2, query, bodyJson) {
|
|
825
|
+
const { apiKey, apiSecret } = requireAuth(config);
|
|
826
|
+
const timestamp = Date.now().toString();
|
|
827
|
+
const params = { ...query, timestamp };
|
|
828
|
+
const sortedKeys = Object.keys(params).sort();
|
|
829
|
+
const queryString = sortedKeys.map((k) => `${k}=${params[k]}`).join("&");
|
|
830
|
+
const pathUrl = `${path2}?${queryString}`;
|
|
831
|
+
let payload = `${method}${pathUrl}`;
|
|
832
|
+
if (bodyJson != null) payload += bodyJson;
|
|
833
|
+
const signature = crypto.createHmac("sha256", apiSecret).update(payload).digest("hex");
|
|
834
|
+
const url = `${config.baseUrl}${pathUrl}`;
|
|
835
|
+
const headers = {
|
|
836
|
+
"PIONEX-KEY": apiKey,
|
|
837
|
+
"PIONEX-SIGNATURE": signature,
|
|
838
|
+
"Content-Type": "application/json"
|
|
839
|
+
};
|
|
840
|
+
return { url, headers, bodyJson };
|
|
841
|
+
}
|
|
842
|
+
async function readTextSafe(res) {
|
|
843
|
+
try {
|
|
844
|
+
return await res.text();
|
|
845
|
+
} catch {
|
|
846
|
+
return "";
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
var PionexRestClient = class {
|
|
850
|
+
config;
|
|
851
|
+
constructor(config) {
|
|
852
|
+
this.config = config;
|
|
853
|
+
}
|
|
854
|
+
async publicGet(path2, query = {}) {
|
|
855
|
+
const qs = buildQueryString(query);
|
|
856
|
+
const endpoint = qs ? `${path2}?${qs}` : path2;
|
|
857
|
+
const url = `${this.config.baseUrl}${endpoint}`;
|
|
858
|
+
const res = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" } });
|
|
859
|
+
if (!res.ok) {
|
|
860
|
+
const txt = await readTextSafe(res);
|
|
861
|
+
throw new PionexApiError(`HTTP ${res.status}: ${txt || res.statusText}`, { status: res.status, endpoint, responseText: txt });
|
|
862
|
+
}
|
|
863
|
+
const data = await res.json();
|
|
864
|
+
return { endpoint, requestTime: (/* @__PURE__ */ new Date()).toISOString(), data };
|
|
865
|
+
}
|
|
866
|
+
async signedGet(path2, query = {}) {
|
|
867
|
+
const { url, headers } = buildSignedRequest(this.config, "GET", path2, query, null);
|
|
868
|
+
const endpoint = `${path2}?${buildQueryString({ ...query, timestamp: "..." })}`;
|
|
869
|
+
const res = await fetch(url, { method: "GET", headers });
|
|
870
|
+
if (!res.ok) {
|
|
871
|
+
const txt = await readTextSafe(res);
|
|
872
|
+
throw new PionexApiError(`HTTP ${res.status}: ${txt || res.statusText}`, { status: res.status, endpoint: path2, responseText: txt });
|
|
873
|
+
}
|
|
874
|
+
const data = await res.json();
|
|
875
|
+
return { endpoint: path2, requestTime: (/* @__PURE__ */ new Date()).toISOString(), data };
|
|
876
|
+
}
|
|
877
|
+
async signedPost(path2, body) {
|
|
878
|
+
const bodyJson = JSON.stringify(body);
|
|
879
|
+
const { url, headers, bodyJson: bj } = buildSignedRequest(this.config, "POST", path2, {}, bodyJson);
|
|
880
|
+
const res = await fetch(url, { method: "POST", headers, body: bj ?? void 0 });
|
|
881
|
+
if (!res.ok) {
|
|
882
|
+
const txt = await readTextSafe(res);
|
|
883
|
+
throw new PionexApiError(`HTTP ${res.status}: ${txt || res.statusText}`, { status: res.status, endpoint: path2, responseText: txt });
|
|
884
|
+
}
|
|
885
|
+
const data = await res.json();
|
|
886
|
+
return { endpoint: path2, requestTime: (/* @__PURE__ */ new Date()).toISOString(), data };
|
|
887
|
+
}
|
|
888
|
+
async signedDelete(path2, body) {
|
|
889
|
+
const bodyJson = JSON.stringify(body);
|
|
890
|
+
const { url, headers, bodyJson: bj } = buildSignedRequest(this.config, "DELETE", path2, {}, bodyJson);
|
|
891
|
+
const res = await fetch(url, { method: "DELETE", headers, body: bj ?? void 0 });
|
|
892
|
+
if (!res.ok) {
|
|
893
|
+
const txt = await readTextSafe(res);
|
|
894
|
+
throw new PionexApiError(`HTTP ${res.status}: ${txt || res.statusText}`, { status: res.status, endpoint: path2, responseText: txt });
|
|
895
|
+
}
|
|
896
|
+
const data = await res.json();
|
|
897
|
+
return { endpoint: path2, requestTime: (/* @__PURE__ */ new Date()).toISOString(), data };
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
function registerMarketTools() {
|
|
901
|
+
return [
|
|
902
|
+
{
|
|
903
|
+
name: "pionex_market_get_depth",
|
|
904
|
+
module: "market",
|
|
905
|
+
isWrite: false,
|
|
906
|
+
description: "Get order book depth (bids and asks) for a symbol. Use for spread, liquidity, or best bid/ask.",
|
|
907
|
+
inputSchema: {
|
|
908
|
+
type: "object",
|
|
909
|
+
additionalProperties: false,
|
|
910
|
+
properties: {
|
|
911
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
912
|
+
limit: { type: "integer", description: "Price levels (1-100), default 5" }
|
|
913
|
+
},
|
|
914
|
+
required: ["symbol"]
|
|
915
|
+
},
|
|
916
|
+
async handler(args, { client }) {
|
|
917
|
+
const symbol = String(args.symbol);
|
|
918
|
+
const limit = args.limit == null ? void 0 : Number(args.limit);
|
|
919
|
+
const q = { symbol };
|
|
920
|
+
if (limit != null && Number.isFinite(limit)) q.limit = limit;
|
|
921
|
+
return (await client.publicGet("/api/v1/market/depth", q)).data;
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
name: "pionex_market_get_trades",
|
|
926
|
+
module: "market",
|
|
927
|
+
isWrite: false,
|
|
928
|
+
description: "Get recent trades for a symbol. Use for latest price and volume.",
|
|
929
|
+
inputSchema: {
|
|
930
|
+
type: "object",
|
|
931
|
+
additionalProperties: false,
|
|
932
|
+
properties: {
|
|
933
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
934
|
+
limit: { type: "integer", description: "Default 5 (1-100)" }
|
|
935
|
+
},
|
|
936
|
+
required: ["symbol"]
|
|
937
|
+
},
|
|
938
|
+
async handler(args, { client }) {
|
|
939
|
+
const symbol = String(args.symbol);
|
|
940
|
+
const limit = args.limit == null ? void 0 : Number(args.limit);
|
|
941
|
+
const q = { symbol };
|
|
942
|
+
if (limit != null && Number.isFinite(limit)) q.limit = limit;
|
|
943
|
+
return (await client.publicGet("/api/v1/market/trades", q)).data;
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: "pionex_market_get_symbol_info",
|
|
948
|
+
module: "market",
|
|
949
|
+
isWrite: false,
|
|
950
|
+
description: "Get symbol metadata (precision, min size, price limits). Call before placing orders to avoid amount/size filter errors.",
|
|
951
|
+
inputSchema: {
|
|
952
|
+
type: "object",
|
|
953
|
+
additionalProperties: false,
|
|
954
|
+
properties: {
|
|
955
|
+
symbols: {
|
|
956
|
+
type: "string",
|
|
957
|
+
description: 'Optional. One or more symbols, comma-separated, e.g. "BTC_USDT" or "BTC_USDT,ADA_USDT".'
|
|
958
|
+
},
|
|
959
|
+
type: {
|
|
960
|
+
type: "string",
|
|
961
|
+
enum: ["SPOT", "PERP"],
|
|
962
|
+
description: "Optional. If no symbols are specified, filter by type (default SPOT)."
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
async handler(args, { client }) {
|
|
967
|
+
const q = {};
|
|
968
|
+
if (args.symbols) q.symbols = String(args.symbols);
|
|
969
|
+
if (!args.symbols && args.type) q.type = String(args.type);
|
|
970
|
+
return (await client.publicGet("/api/v1/common/symbols", q)).data;
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: "pionex_market_get_tickers",
|
|
975
|
+
module: "market",
|
|
976
|
+
isWrite: false,
|
|
977
|
+
description: "Get 24-hour ticker(s): open, close, high, low, volume, amount, count. Optional symbol or type (SPOT/PERP).",
|
|
978
|
+
inputSchema: {
|
|
979
|
+
type: "object",
|
|
980
|
+
additionalProperties: false,
|
|
981
|
+
properties: {
|
|
982
|
+
symbol: { type: "string", description: "e.g. BTC_USDT; if omitted, returns all tickers filtered by type" },
|
|
983
|
+
type: { type: "string", enum: ["SPOT", "PERP"], description: "If symbol is not specified, filter by type." }
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
async handler(args, { client }) {
|
|
987
|
+
const q = {};
|
|
988
|
+
if (args.symbol) q.symbol = String(args.symbol);
|
|
989
|
+
if (args.type) q.type = String(args.type);
|
|
990
|
+
return (await client.publicGet("/api/v1/market/tickers", q)).data;
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
name: "pionex_market_get_klines",
|
|
995
|
+
module: "market",
|
|
996
|
+
isWrite: false,
|
|
997
|
+
description: "Get OHLCV klines (candlestick) for a symbol. Use for charts or historical price/volume.",
|
|
998
|
+
inputSchema: {
|
|
999
|
+
type: "object",
|
|
1000
|
+
additionalProperties: false,
|
|
1001
|
+
properties: {
|
|
1002
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
1003
|
+
interval: { type: "string", enum: ["1M", "5M", "15M", "30M", "60M", "4H", "8H", "12H", "1D"], description: "Kline interval." },
|
|
1004
|
+
endTime: { type: "integer", description: "End time in milliseconds." },
|
|
1005
|
+
limit: { type: "integer", description: "Default 100 (1-500)." }
|
|
1006
|
+
},
|
|
1007
|
+
required: ["symbol", "interval"]
|
|
1008
|
+
},
|
|
1009
|
+
async handler(args, { client }) {
|
|
1010
|
+
const symbol = String(args.symbol);
|
|
1011
|
+
const interval = String(args.interval);
|
|
1012
|
+
const q = { symbol, interval };
|
|
1013
|
+
if (args.endTime != null) q.endTime = Number(args.endTime);
|
|
1014
|
+
if (args.limit != null) q.limit = Number(args.limit);
|
|
1015
|
+
return (await client.publicGet("/api/v1/market/klines", q)).data;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
];
|
|
1019
|
+
}
|
|
1020
|
+
function registerAccountTools() {
|
|
1021
|
+
return [
|
|
1022
|
+
{
|
|
1023
|
+
name: "pionex_account_get_balance",
|
|
1024
|
+
module: "account",
|
|
1025
|
+
isWrite: false,
|
|
1026
|
+
description: "Query spot account balances for all currencies. Requires API key and secret in ~/.pionex/config.toml or env.",
|
|
1027
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
1028
|
+
async handler(_args, { client }) {
|
|
1029
|
+
return (await client.signedGet("/api/v1/account/balances")).data;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
1034
|
+
function registerOrdersTools() {
|
|
1035
|
+
return [
|
|
1036
|
+
{
|
|
1037
|
+
name: "pionex_orders_new_order",
|
|
1038
|
+
module: "orders",
|
|
1039
|
+
isWrite: true,
|
|
1040
|
+
description: "Create a spot order on Pionex. LIMIT requires symbol/side/type=LIMIT/price/size. MARKET BUY requires amount (quote). MARKET SELL requires size (base).",
|
|
1041
|
+
inputSchema: {
|
|
1042
|
+
type: "object",
|
|
1043
|
+
additionalProperties: false,
|
|
1044
|
+
properties: {
|
|
1045
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
1046
|
+
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
1047
|
+
type: { type: "string", enum: ["LIMIT", "MARKET"] },
|
|
1048
|
+
clientOrderId: { type: "string", description: "Optional client order id (max 64 chars)" },
|
|
1049
|
+
size: { type: "string", description: "Quantity; required for limit and market sell" },
|
|
1050
|
+
price: { type: "string", description: "Required for limit order" },
|
|
1051
|
+
amount: { type: "string", description: "Quote amount; required for market buy" },
|
|
1052
|
+
IOC: { type: "boolean", description: "Immediate-or-cancel, default false" }
|
|
1053
|
+
},
|
|
1054
|
+
required: ["symbol", "side", "type"]
|
|
1055
|
+
},
|
|
1056
|
+
async handler(args, { client, config }) {
|
|
1057
|
+
if (config.readOnly) {
|
|
1058
|
+
throw new Error("Server is running in --read-only mode; order placement is disabled.");
|
|
1059
|
+
}
|
|
1060
|
+
const body = {};
|
|
1061
|
+
if (args.symbol != null) body.symbol = String(args.symbol);
|
|
1062
|
+
if (args.side != null) body.side = String(args.side);
|
|
1063
|
+
if (args.type != null) body.type = String(args.type);
|
|
1064
|
+
if (args.clientOrderId != null) body.clientOrderId = String(args.clientOrderId);
|
|
1065
|
+
if (args.size != null) body.size = String(args.size);
|
|
1066
|
+
if (args.price != null) body.price = String(args.price);
|
|
1067
|
+
if (args.amount != null) body.amount = String(args.amount);
|
|
1068
|
+
if (args.IOC != null) body.IOC = Boolean(args.IOC);
|
|
1069
|
+
return (await client.signedPost("/api/v1/trade/order", body)).data;
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
name: "pionex_orders_get_order",
|
|
1074
|
+
module: "orders",
|
|
1075
|
+
isWrite: false,
|
|
1076
|
+
description: "Get a single order by order ID.",
|
|
1077
|
+
inputSchema: {
|
|
1078
|
+
type: "object",
|
|
1079
|
+
additionalProperties: false,
|
|
1080
|
+
properties: {
|
|
1081
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
1082
|
+
orderId: { type: "integer", description: "Order id" }
|
|
1083
|
+
},
|
|
1084
|
+
required: ["symbol", "orderId"]
|
|
1085
|
+
},
|
|
1086
|
+
async handler(args, { client }) {
|
|
1087
|
+
const symbol = String(args.symbol);
|
|
1088
|
+
const orderId = Number(args.orderId);
|
|
1089
|
+
return (await client.signedGet("/api/v1/trade/order", { symbol, orderId })).data;
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: "pionex_orders_get_order_by_client_order_id",
|
|
1094
|
+
module: "orders",
|
|
1095
|
+
isWrite: false,
|
|
1096
|
+
description: "Get a single order by client order ID.",
|
|
1097
|
+
inputSchema: {
|
|
1098
|
+
type: "object",
|
|
1099
|
+
additionalProperties: false,
|
|
1100
|
+
properties: {
|
|
1101
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
1102
|
+
clientOrderId: { type: "string", description: "Client order id" }
|
|
1103
|
+
},
|
|
1104
|
+
required: ["symbol", "clientOrderId"]
|
|
1105
|
+
},
|
|
1106
|
+
async handler(args, { client }) {
|
|
1107
|
+
const symbol = String(args.symbol);
|
|
1108
|
+
const clientOrderId = String(args.clientOrderId);
|
|
1109
|
+
return (await client.signedGet("/api/v1/trade/orderByClientOrderId", { symbol, clientOrderId })).data;
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: "pionex_orders_get_open_orders",
|
|
1114
|
+
module: "orders",
|
|
1115
|
+
isWrite: false,
|
|
1116
|
+
description: "List open (unfilled) orders for a symbol.",
|
|
1117
|
+
inputSchema: {
|
|
1118
|
+
type: "object",
|
|
1119
|
+
additionalProperties: false,
|
|
1120
|
+
properties: { symbol: { type: "string", description: "e.g. BTC_USDT" } },
|
|
1121
|
+
required: ["symbol"]
|
|
1122
|
+
},
|
|
1123
|
+
async handler(args, { client }) {
|
|
1124
|
+
const symbol = String(args.symbol);
|
|
1125
|
+
return (await client.signedGet("/api/v1/trade/openOrders", { symbol })).data;
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: "pionex_orders_get_all_orders",
|
|
1130
|
+
module: "orders",
|
|
1131
|
+
isWrite: false,
|
|
1132
|
+
description: "List order history for a symbol (filled and cancelled), with optional limit.",
|
|
1133
|
+
inputSchema: {
|
|
1134
|
+
type: "object",
|
|
1135
|
+
additionalProperties: false,
|
|
1136
|
+
properties: {
|
|
1137
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
1138
|
+
limit: { type: "integer", description: "Default 1 (1-100)" }
|
|
1139
|
+
},
|
|
1140
|
+
required: ["symbol"]
|
|
1141
|
+
},
|
|
1142
|
+
async handler(args, { client }) {
|
|
1143
|
+
const symbol = String(args.symbol);
|
|
1144
|
+
const q = { symbol };
|
|
1145
|
+
if (args.limit != null) q.limit = Number(args.limit);
|
|
1146
|
+
return (await client.signedGet("/api/v1/trade/allOrders", q)).data;
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: "pionex_orders_cancel_order",
|
|
1151
|
+
module: "orders",
|
|
1152
|
+
isWrite: true,
|
|
1153
|
+
description: "Cancel an open order by order ID.",
|
|
1154
|
+
inputSchema: {
|
|
1155
|
+
type: "object",
|
|
1156
|
+
additionalProperties: false,
|
|
1157
|
+
properties: {
|
|
1158
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
1159
|
+
orderId: { type: "integer", description: "Order id" }
|
|
1160
|
+
},
|
|
1161
|
+
required: ["symbol", "orderId"]
|
|
1162
|
+
},
|
|
1163
|
+
async handler(args, { client, config }) {
|
|
1164
|
+
if (config.readOnly) {
|
|
1165
|
+
throw new Error("Server is running in --read-only mode; order cancellation is disabled.");
|
|
1166
|
+
}
|
|
1167
|
+
const symbol = String(args.symbol);
|
|
1168
|
+
const orderId = Number(args.orderId);
|
|
1169
|
+
return (await client.signedDelete("/api/v1/trade/order", { symbol, orderId })).data;
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
name: "pionex_orders_get_fills",
|
|
1174
|
+
module: "orders",
|
|
1175
|
+
isWrite: false,
|
|
1176
|
+
description: "Get filled trades (fills) for a symbol in a time range. Requires API key. Returns up to 100 latest fills.",
|
|
1177
|
+
inputSchema: {
|
|
1178
|
+
type: "object",
|
|
1179
|
+
additionalProperties: false,
|
|
1180
|
+
properties: {
|
|
1181
|
+
symbol: { type: "string", description: "e.g. BTC_USDT" },
|
|
1182
|
+
startTime: { type: "integer", description: "Start time in milliseconds." },
|
|
1183
|
+
endTime: { type: "integer", description: "End time in milliseconds." }
|
|
1184
|
+
},
|
|
1185
|
+
required: ["symbol"]
|
|
1186
|
+
},
|
|
1187
|
+
async handler(args, { client }) {
|
|
1188
|
+
const symbol = String(args.symbol);
|
|
1189
|
+
const q = { symbol };
|
|
1190
|
+
if (args.startTime != null) q.startTime = Number(args.startTime);
|
|
1191
|
+
if (args.endTime != null) q.endTime = Number(args.endTime);
|
|
1192
|
+
return (await client.signedGet("/api/v1/trade/fills", q)).data;
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
name: "pionex_orders_cancel_all_orders",
|
|
1197
|
+
module: "orders",
|
|
1198
|
+
isWrite: true,
|
|
1199
|
+
description: "Cancel all open orders for a symbol.",
|
|
1200
|
+
inputSchema: {
|
|
1201
|
+
type: "object",
|
|
1202
|
+
additionalProperties: false,
|
|
1203
|
+
properties: { symbol: { type: "string", description: "e.g. BTC_USDT" } },
|
|
1204
|
+
required: ["symbol"]
|
|
1205
|
+
},
|
|
1206
|
+
async handler(args, { client, config }) {
|
|
1207
|
+
if (config.readOnly) {
|
|
1208
|
+
throw new Error("Server is running in --read-only mode; cancel-all is disabled.");
|
|
1209
|
+
}
|
|
1210
|
+
const symbol = String(args.symbol);
|
|
1211
|
+
return (await client.signedDelete("/api/v1/trade/allOrders", { symbol })).data;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
];
|
|
1215
|
+
}
|
|
1216
|
+
function allToolSpecs() {
|
|
1217
|
+
return [...registerMarketTools(), ...registerAccountTools(), ...registerOrdersTools()];
|
|
1218
|
+
}
|
|
1219
|
+
function buildTools(config) {
|
|
1220
|
+
const enabled = new Set(config.modules);
|
|
1221
|
+
const tools = allToolSpecs().filter((t) => enabled.has(t.module));
|
|
1222
|
+
if (!config.readOnly) return tools;
|
|
1223
|
+
return tools.filter((t) => !t.isWrite);
|
|
1224
|
+
}
|
|
1225
|
+
function toMcpTool(tool) {
|
|
1226
|
+
return {
|
|
1227
|
+
name: tool.name,
|
|
1228
|
+
description: tool.description,
|
|
1229
|
+
inputSchema: tool.inputSchema,
|
|
1230
|
+
annotations: {
|
|
1231
|
+
readOnlyHint: !tool.isWrite,
|
|
1232
|
+
destructiveHint: tool.isWrite,
|
|
1233
|
+
idempotentHint: !tool.isWrite,
|
|
1234
|
+
openWorldHint: false
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
734
1237
|
}
|
|
735
|
-
async function main() {
|
|
736
|
-
if (process.argv[2] === "--help" || process.argv[2] === "-h") {
|
|
737
|
-
process.stdout.write(
|
|
738
|
-
`Usage: pionex-trade-mcp
|
|
739
1238
|
|
|
740
|
-
|
|
741
|
-
|
|
1239
|
+
// src/server.ts
|
|
1240
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1241
|
+
import {
|
|
1242
|
+
CallToolRequestSchema,
|
|
1243
|
+
ListToolsRequestSchema
|
|
1244
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
1245
|
+
var SERVER_NAME = "pionex-trade-mcp";
|
|
1246
|
+
var SERVER_VERSION = "0.3.0";
|
|
1247
|
+
var SYSTEM_CAPABILITIES_TOOL_NAME = "system_get_capabilities";
|
|
1248
|
+
var SYSTEM_CAPABILITIES_TOOL = {
|
|
1249
|
+
name: SYSTEM_CAPABILITIES_TOOL_NAME,
|
|
1250
|
+
description: "Return machine-readable server capabilities and module availability for agent planning.",
|
|
1251
|
+
inputSchema: { type: "object", additionalProperties: false },
|
|
1252
|
+
annotations: {
|
|
1253
|
+
readOnlyHint: true,
|
|
1254
|
+
destructiveHint: false,
|
|
1255
|
+
idempotentHint: true,
|
|
1256
|
+
openWorldHint: false
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
function buildCapabilitySnapshot(config) {
|
|
1260
|
+
const enabledModules = new Set(config.modules);
|
|
1261
|
+
const moduleAvailability = {};
|
|
1262
|
+
for (const moduleId of MODULES) {
|
|
1263
|
+
if (!enabledModules.has(moduleId)) {
|
|
1264
|
+
moduleAvailability[moduleId] = { status: "disabled", reasonCode: "MODULE_FILTERED" };
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (moduleId === "market") {
|
|
1268
|
+
moduleAvailability[moduleId] = { status: "enabled" };
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (!config.hasAuth) {
|
|
1272
|
+
moduleAvailability[moduleId] = { status: "requires_auth", reasonCode: "AUTH_MISSING" };
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
moduleAvailability[moduleId] = { status: "enabled" };
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
readOnly: config.readOnly,
|
|
1279
|
+
hasAuth: config.hasAuth,
|
|
1280
|
+
moduleAvailability
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function successResult(toolName, data, snapshot) {
|
|
1284
|
+
const payload = {
|
|
1285
|
+
tool: toolName,
|
|
1286
|
+
ok: true,
|
|
1287
|
+
data,
|
|
1288
|
+
capabilities: snapshot,
|
|
1289
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1290
|
+
};
|
|
1291
|
+
return {
|
|
1292
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
1293
|
+
structuredContent: payload
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function errorResult(toolName, error, snapshot) {
|
|
1297
|
+
const payload = toToolErrorPayload(error);
|
|
1298
|
+
const structured = {
|
|
1299
|
+
tool: toolName,
|
|
1300
|
+
...payload,
|
|
1301
|
+
serverVersion: SERVER_VERSION,
|
|
1302
|
+
capabilities: snapshot
|
|
1303
|
+
};
|
|
1304
|
+
return {
|
|
1305
|
+
isError: true,
|
|
1306
|
+
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
|
|
1307
|
+
structuredContent: structured
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function createServer(config) {
|
|
1311
|
+
const client = new PionexRestClient(config);
|
|
1312
|
+
const tools = buildTools(config);
|
|
1313
|
+
const toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
1314
|
+
const server = new Server(
|
|
1315
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
1316
|
+
{ capabilities: { tools: {} } }
|
|
1317
|
+
);
|
|
1318
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1319
|
+
return { tools: [...tools.map(toMcpTool), SYSTEM_CAPABILITIES_TOOL] };
|
|
1320
|
+
});
|
|
1321
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1322
|
+
const toolName = request.params.name;
|
|
1323
|
+
const snapshot = buildCapabilitySnapshot(config);
|
|
1324
|
+
if (toolName === SYSTEM_CAPABILITIES_TOOL_NAME) {
|
|
1325
|
+
return successResult(
|
|
1326
|
+
toolName,
|
|
1327
|
+
{
|
|
1328
|
+
server: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
1329
|
+
capabilities: snapshot
|
|
1330
|
+
},
|
|
1331
|
+
snapshot
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
const tool = toolMap.get(toolName);
|
|
1335
|
+
if (!tool) {
|
|
1336
|
+
return errorResult(toolName, new Error(`Tool "${toolName}" is not available in this server session.`), snapshot);
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
const data = await tool.handler(request.params.arguments ?? {}, { config, client });
|
|
1340
|
+
return successResult(toolName, data, snapshot);
|
|
1341
|
+
} catch (e) {
|
|
1342
|
+
return errorResult(toolName, e, snapshot);
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
return server;
|
|
1346
|
+
}
|
|
742
1347
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
`
|
|
746
|
-
|
|
1348
|
+
// src/index.ts
|
|
1349
|
+
function printHelp() {
|
|
1350
|
+
process.stdout.write(`
|
|
1351
|
+
Usage: pionex-trade-mcp [options]
|
|
1352
|
+
|
|
1353
|
+
Options:
|
|
1354
|
+
--modules <list> Comma-separated list of modules to load
|
|
1355
|
+
Available: market, account, orders
|
|
1356
|
+
Special: "all" loads all modules
|
|
1357
|
+
Default: market,account,orders
|
|
1358
|
+
|
|
1359
|
+
--profile <name> Profile to load from ~/.pionex/config.toml
|
|
1360
|
+
Falls back to default_profile in config, then "default"
|
|
1361
|
+
--base-url <url> Override API base URL (otherwise env PIONEX_BASE_URL / toml / default)
|
|
1362
|
+
--read-only Expose only read/query tools and disable write operations
|
|
1363
|
+
--help Show this help message
|
|
1364
|
+
--version Show version
|
|
1365
|
+
|
|
1366
|
+
Credentials (priority: env vars > ~/.pionex/config.toml > none):
|
|
1367
|
+
PIONEX_API_KEY Pionex API key
|
|
1368
|
+
PIONEX_API_SECRET Pionex API secret
|
|
1369
|
+
PIONEX_BASE_URL Optional API base URL override
|
|
1370
|
+
`);
|
|
1371
|
+
}
|
|
1372
|
+
function parseCli() {
|
|
1373
|
+
const parsed = parseArgs({
|
|
1374
|
+
options: {
|
|
1375
|
+
modules: { type: "string" },
|
|
1376
|
+
profile: { type: "string" },
|
|
1377
|
+
"base-url": { type: "string" },
|
|
1378
|
+
"read-only": { type: "boolean", default: false },
|
|
1379
|
+
help: { type: "boolean", default: false },
|
|
1380
|
+
version: { type: "boolean", default: false }
|
|
1381
|
+
},
|
|
1382
|
+
allowPositionals: false
|
|
1383
|
+
});
|
|
1384
|
+
return {
|
|
1385
|
+
modules: parsed.values.modules,
|
|
1386
|
+
profile: parsed.values.profile,
|
|
1387
|
+
baseUrl: parsed.values["base-url"],
|
|
1388
|
+
readOnly: parsed.values["read-only"] ?? false,
|
|
1389
|
+
help: parsed.values.help ?? false,
|
|
1390
|
+
version: parsed.values.version ?? false
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
async function main() {
|
|
1394
|
+
const cli = parseCli();
|
|
1395
|
+
if (cli.help) {
|
|
1396
|
+
printHelp();
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (cli.version) {
|
|
1400
|
+
process.stdout.write(`pionex-trade-mcp
|
|
1401
|
+
`);
|
|
747
1402
|
return;
|
|
748
1403
|
}
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1404
|
+
const config = loadConfig({
|
|
1405
|
+
modules: cli.modules,
|
|
1406
|
+
profile: cli.profile,
|
|
1407
|
+
baseUrl: cli.baseUrl,
|
|
1408
|
+
readOnly: cli.readOnly
|
|
1409
|
+
});
|
|
1410
|
+
const server = createServer(config);
|
|
1411
|
+
const transport = new StdioServerTransport();
|
|
1412
|
+
await server.connect(transport);
|
|
753
1413
|
}
|
|
754
|
-
main().catch((
|
|
755
|
-
|
|
756
|
-
process.
|
|
1414
|
+
main().catch((error) => {
|
|
1415
|
+
const payload = toToolErrorPayload(error);
|
|
1416
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}
|
|
1417
|
+
`);
|
|
1418
|
+
process.exitCode = 1;
|
|
757
1419
|
});
|
|
1420
|
+
export {
|
|
1421
|
+
main
|
|
1422
|
+
};
|
|
758
1423
|
/*! Bundled license information:
|
|
759
1424
|
|
|
760
1425
|
smol-toml/dist/error.js:
|