@definitive-fi/mcp 1.2.0 → 1.3.0

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/server.js +155 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,5 +33,5 @@ New to Definitive? Visit the [Definitive API documentation](https://ddp.definiti
33
33
  | `DEFINITIVE_API_KEY` | Yes | Your Definitive API key |
34
34
  | `DEFINITIVE_API_SECRET` | Yes | Your Definitive API secret |
35
35
  | `DEFINITIVE_API_KEY_TYPE` | Yes | Key type: `"portfolio"` or `"organization"` |
36
- | `DEFINITIVE_BASE_URL` | No | API base URL |
36
+ | `DEFINITIVE_BASE_URL` | No | API base URL. Must use `https`; defaults to `https://ddp.definitive.fi`. A different host prints a warning at startup |
37
37
  | `DEFINITIVE_PORTFOLIO_ID` | No | Portfolio ID (required for organization keys when targeting a specific portfolio) |
package/dist/server.js CHANGED
@@ -76,16 +76,28 @@ function createClient(baseUrl, apiKey, apiSecret) {
76
76
  };
77
77
  }
78
78
  if (!response.ok) {
79
+ const issues = validationIssues(response.status, json);
79
80
  return {
80
81
  error: true,
81
82
  status_code: response.status,
82
- message: typeof json === "object" && json !== null && "message" in json ? String(json.message) : response.statusText
83
+ message: typeof json === "object" && json !== null && "message" in json ? String(json.message) : response.statusText,
84
+ ...issues && { details: issues }
83
85
  };
84
86
  }
85
87
  return json;
86
88
  }
87
89
  };
88
90
  }
91
+ function validationIssues(status, json) {
92
+ if (status !== 400 || typeof json !== "object" || json === null) {
93
+ return;
94
+ }
95
+ const error = "error" in json ? json.error : undefined;
96
+ if (typeof error !== "object" || error === null || !("issues" in error)) {
97
+ return;
98
+ }
99
+ return Array.isArray(error.issues) ? error.issues : undefined;
100
+ }
89
101
 
90
102
 
91
103
  function resolvePortfolioRoute(keyType, suffix, portfolioId, defaultPortfolioId) {
@@ -103,29 +115,16 @@ function resolvePortfolioRoute(keyType, suffix, portfolioId, defaultPortfolioId)
103
115
  import { z } from "zod";
104
116
  var zUUID = z.string().uuid();
105
117
  var portfolioIdParam = zUUID.optional().describe("Portfolio UUID (required for organization keys without a default)");
106
- var ChainEnum = z.enum([
107
- "arbitrum",
108
- "avalanche",
109
- "base",
110
- "blast",
111
- "bsc",
112
- "ethereum",
113
- "optimism",
114
- "polygon",
115
- "solana",
116
- "hyperevm",
117
- "plasma",
118
- "monad",
119
- "robinhood",
120
- "ink"
121
- ]);
118
+ var chainParam = (label) => z.string().min(1).describe(`${label}, lowercase (e.g. ethereum, base, arbitrum, solana)`);
122
119
  var jsonResult = (result) => ({
123
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
120
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
121
+ ...isErrorResult(result) && { isError: true }
124
122
  });
123
+ var isErrorResult = (result) => typeof result === "object" && result !== null && ("error" in result) && result.error === true;
125
124
 
126
125
 
127
126
  import { z as z2 } from "zod";
128
- var perpsSymbolParam = z2.string().min(1).max(32).describe('Perps market symbol. Qualify with the subvenue when the base symbol is ambiguous, e.g. "native:BTC" (core exchange) — a bare symbol works only when exactly one market uses it');
127
+ var perpsSymbolParam = z2.string().min(1).max(32).regex(/^[A-Za-z0-9:._-]+$/, "must contain only letters, digits, and : . _ -").describe('Perps market symbol. Qualify with the subvenue when the base symbol is ambiguous, e.g. "native:BTC" (core exchange) — a bare symbol works only when exactly one market uses it');
129
128
  var MIN_EPOCH_MS = 1600000000000;
130
129
  var MAX_EPOCH_MS = 4102444800000;
131
130
  var epochMsParam = () => z2.number().int().min(MIN_EPOCH_MS).max(MAX_EPOCH_MS);
@@ -232,6 +231,125 @@ function registerPerpsReadTools(server, client, keyType, defaultPortfolioId) {
232
231
  return jsonResult(result);
233
232
  });
234
233
  }
234
+ var evmAddressParam = z2.string().regex(/^0x[a-fA-F0-9]{40}$/, "must be a 0x-prefixed 20-byte hex address");
235
+ var hex32Param = z2.string().regex(/^0x[a-fA-F0-9]{64}$/, "must be a 0x-prefixed 32-byte hex string");
236
+ var depositFields = {
237
+ source_vault_id: zUUID.describe("Trading vault that sends the funds: vaults[].vaultId from get_portfolio"),
238
+ destination_portfolio_id: zUUID.describe("Portfolio whose perps account receives the funds; it must already have a perps account (see perps_get_account_status)"),
239
+ from_asset_address: z2.string().min(1).describe("Contract address of the asset to send, on the source vault's chain"),
240
+ from_amount: decimalStringParam.describe('Amount of the asset to send, decimal string (e.g. "250")'),
241
+ portfolio_id: portfolioIdParam
242
+ };
243
+ function registerPerpsWriteTools(server, client, keyType, defaultPortfolioId) {
244
+ const resolve = (suffix, portfolioId) => resolvePortfolioRoute(keyType, `perps/${suffix}`, portfolioId, defaultPortfolioId);
245
+ server.tool("perps_preview_order", "Validate a proposed perps order against the live account and preview its cost, margin, and fees. Nothing is placed. Requires a WRITE-scoped key.", {
246
+ symbol: perpsSymbolParam,
247
+ side: z2.enum(["buy", "sell"]).describe("Order side"),
248
+ order_type: z2.enum([
249
+ "market",
250
+ "limit",
251
+ "stop_market",
252
+ "stop_limit",
253
+ "take_profit_market",
254
+ "take_profit_limit",
255
+ "twap"
256
+ ]).describe("Order type"),
257
+ size: decimalStringParam.describe('Order size in the base asset, decimal string (e.g. "0.5")'),
258
+ price: decimalStringParam.optional().describe("Limit price, for limit-priced order types"),
259
+ trigger_price: decimalStringParam.optional().describe("Trigger price, for stop and take-profit order types"),
260
+ reduce_only: z2.boolean().optional().describe("Only reduce the current position"),
261
+ leverage: z2.number().int().min(1).max(100).optional().describe("Leverage 1-100; defaults to the account's active leverage"),
262
+ take_profit: decimalStringParam.optional().describe("Attached take-profit price"),
263
+ stop_loss: decimalStringParam.optional().describe("Attached stop-loss price"),
264
+ twap_duration_minutes: z2.number().int().min(5).max(1440).optional().describe("TWAP duration in minutes, 5-1440; required for twap"),
265
+ portfolio_id: portfolioIdParam
266
+ }, async ({
267
+ symbol,
268
+ side,
269
+ order_type,
270
+ size,
271
+ price,
272
+ trigger_price,
273
+ reduce_only,
274
+ leverage,
275
+ take_profit,
276
+ stop_loss,
277
+ twap_duration_minutes,
278
+ portfolio_id
279
+ }) => {
280
+ const result = await client.request("POST", resolve("orders/preview", portfolio_id), undefined, {
281
+ symbol,
282
+ side,
283
+ orderType: order_type,
284
+ size,
285
+ price,
286
+ triggerPrice: trigger_price,
287
+ reduceOnly: reduce_only,
288
+ leverage,
289
+ takeProfit: take_profit,
290
+ stopLoss: stop_loss,
291
+ twapDurationMinutes: twap_duration_minutes
292
+ });
293
+ return jsonResult(result);
294
+ });
295
+ server.tool("perps_get_withdraw_payload", "Step 1 of a perps withdrawal (perps account -> the portfolio's Arbitrum trading vault, in USDC). Returns EIP-712 typedDataJson and its nonce. The payload must be signed by the perps account owner wallet (accountOwnerAddress from perps_get_account_status); this server cannot sign. Pass the signature to perps_submit_withdraw. Requires a WRITE-scoped key.", {
296
+ amount: decimalStringParam.describe('USDC amount to withdraw, decimal string (e.g. "250"); the venue deducts its ~$1 fee from it'),
297
+ destination: evmAddressParam.optional().describe("Arbitrum address to receive the USDC; must be one of the portfolio's Arbitrum trading vaults. Defaults to the portfolio's Arbitrum trading vault"),
298
+ portfolio_id: portfolioIdParam
299
+ }, async ({ amount, destination, portfolio_id }) => {
300
+ const result = await client.request("POST", resolve("withdraw/payload", portfolio_id), undefined, { amount, destination });
301
+ return jsonResult(result);
302
+ });
303
+ server.tool("perps_submit_withdraw", "Step 2 of a perps withdrawal: submit the account owner's signature over the typed data from perps_get_withdraw_payload. amount, destination, and nonce must be exactly the values that step returned. USDC arrives on Arbitrum in about five minutes. Requires a WRITE-scoped key.", {
304
+ amount: decimalStringParam.describe("Exactly the amount returned by perps_get_withdraw_payload"),
305
+ destination: evmAddressParam.describe("Exactly the destination returned by perps_get_withdraw_payload"),
306
+ nonce: epochMsParam().describe("Exactly the nonce returned by perps_get_withdraw_payload"),
307
+ signature: z2.object({
308
+ r: hex32Param,
309
+ s: hex32Param,
310
+ v: z2.union([z2.literal(27), z2.literal(28)])
311
+ }).describe("The account owner's EIP-712 signature split into r, s (0x-prefixed 32-byte hex) and v (27 or 28)"),
312
+ portfolio_id: portfolioIdParam
313
+ }, async ({ amount, destination, nonce, signature, portfolio_id }) => {
314
+ const result = await client.request("POST", resolve("withdraw", portfolio_id), undefined, { amount, destination, nonce, signature });
315
+ return jsonResult(result);
316
+ });
317
+ server.tool("perps_get_deposit_quote", "Step 1 of a perps deposit (trading vault -> perps account). Prices the transfer and returns a quoteId plus toAmount, the amount that lands after routing and gas. No wallet signature is needed. Pass the same fields and the quoteId to perps_deposit. Requires a WRITE-scoped key.", depositFields, async ({
318
+ source_vault_id,
319
+ destination_portfolio_id,
320
+ from_asset_address,
321
+ from_amount,
322
+ portfolio_id
323
+ }) => {
324
+ const result = await client.request("POST", resolve("deposit/vault-fund-hl/quote", portfolio_id), undefined, {
325
+ sourceVaultId: source_vault_id,
326
+ destinationPortfolioId: destination_portfolio_id,
327
+ fromAssetAddress: from_asset_address,
328
+ fromAmount: from_amount
329
+ });
330
+ return jsonResult(result);
331
+ });
332
+ server.tool("perps_deposit", "Step 2 of a perps deposit: execute a quote from perps_get_deposit_quote. Repeat the quote's four fields exactly and add its quoteId. Settles asynchronously and returns a requestId; poll perps_get_account for the new balance. Moves real funds. Requires a WRITE-scoped key.", {
333
+ quote_id: z2.string().min(1).describe("The quoteId returned by perps_get_deposit_quote"),
334
+ ...depositFields
335
+ }, async ({
336
+ quote_id,
337
+ source_vault_id,
338
+ destination_portfolio_id,
339
+ from_asset_address,
340
+ from_amount,
341
+ portfolio_id
342
+ }) => {
343
+ const result = await client.request("POST", resolve("deposit/vault-fund-hl", portfolio_id), undefined, {
344
+ quoteId: quote_id,
345
+ sourceVaultId: source_vault_id,
346
+ destinationPortfolioId: destination_portfolio_id,
347
+ fromAssetAddress: from_asset_address,
348
+ fromAmount: from_amount
349
+ });
350
+ return jsonResult(result);
351
+ });
352
+ }
235
353
 
236
354
 
237
355
  import { z as z3 } from "zod";
@@ -294,7 +412,7 @@ function registerReadTools(server, client, keyType, defaultPortfolioId) {
294
412
  return jsonResult(result);
295
413
  });
296
414
  server.tool("get_deposit_address", "Get a deposit address for a specific blockchain. Creates a vault if one doesn't exist for that chain.", {
297
- chain: ChainEnum.describe("Blockchain network name"),
415
+ chain: chainParam("Blockchain network name"),
298
416
  wallet_address: z3.string().min(1).describe("User's wallet address on this chain — ask the user if not provided"),
299
417
  portfolio_id: portfolioIdParam
300
418
  }, async ({ chain, wallet_address, portfolio_id }) => {
@@ -374,7 +492,7 @@ function registerWriteTools(server, client, keyType, defaultPortfolioId) {
374
492
  const resolve = (suffix, portfolioId) => resolvePortfolioRoute(keyType, suffix, portfolioId, defaultPortfolioId);
375
493
  server.tool("get_trade_quote", "Get a price quote for a trade order. This is the DEFAULT tool for all trades — use this unless the user explicitly asks for a QuickTrade. Supports market, limit, TWAP, stop, stop-loss, and take-profit order types. This does not execute any trade. Use the returned quote ID with submit_trade to execute.", {
376
494
  type: OrderTypeEnum.describe("Order type"),
377
- chain: ChainEnum.describe("Blockchain network name"),
495
+ chain: chainParam("Blockchain network name"),
378
496
  target_asset: z4.string().min(1).describe("Target asset contract address"),
379
497
  contra_asset: z4.string().min(1).describe("Contra (quote) asset contract address"),
380
498
  qty: z4.string().min(1).describe("Order quantity as a decimal string"),
@@ -444,7 +562,7 @@ function registerWriteTools(server, client, keyType, defaultPortfolioId) {
444
562
  server.tool("get_quicktrade_quote", "Get a price quote for a QuickTrade market swap. Only use when the user explicitly requests a QuickTrade. This does not execute any trade. Review the quote before executing with the quicktrade tool.", {
445
563
  target_asset: z4.string().min(1).describe("Target asset contract address"),
446
564
  contra_asset: z4.string().min(1).describe("Contra (quote) asset contract address"),
447
- chain: ChainEnum.describe("Blockchain network name"),
565
+ chain: chainParam("Blockchain network name"),
448
566
  qty: z4.string().min(1).describe("Order quantity as a decimal string"),
449
567
  order_side: OrderSideEnum.describe("Buy or sell"),
450
568
  portfolio_id: portfolioIdParam
@@ -470,9 +588,9 @@ function registerWriteTools(server, client, keyType, defaultPortfolioId) {
470
588
  });
471
589
  server.tool("bridge_quote", "Get a quote for a cross-chain bridge transfer. This does not move any funds. Review the routes and select a route_id before executing with bridge_submit.", {
472
590
  from_asset_address: z4.string().min(1).describe("Source asset contract address"),
473
- from_chain: ChainEnum.describe("Source blockchain network"),
591
+ from_chain: chainParam("Source blockchain network"),
474
592
  to_asset_address: z4.string().min(1).describe("Destination asset contract address"),
475
- to_chain: ChainEnum.describe("Destination blockchain network"),
593
+ to_chain: chainParam("Destination blockchain network"),
476
594
  from_amount: z4.string().min(1).describe("Amount to bridge as a decimal string"),
477
595
  portfolio_id: portfolioIdParam
478
596
  }, { readOnlyHint: true }, async ({
@@ -519,7 +637,7 @@ function registerWriteTools(server, client, keyType, defaultPortfolioId) {
519
637
  server.tool("submit_trade", "Submit a trade order using a quote from get_trade_quote. This is irreversible — all order types (market, limit, stop, TWAP) may execute immediately. Always show the user the quote details and get confirmation before calling this tool. Pass the same parameters you used for get_trade_quote plus the quote_id.", {
520
638
  quote_id: zUUID.describe("Quote UUID from get_trade_quote response (found in quote.quote.id)"),
521
639
  type: OrderTypeEnum.describe("Order type"),
522
- chain: ChainEnum.describe("Blockchain network name"),
640
+ chain: chainParam("Blockchain network name"),
523
641
  target_asset: z4.string().min(1).describe("Target asset contract address"),
524
642
  contra_asset: z4.string().min(1).describe("Contra (quote) asset contract address"),
525
643
  qty: z4.string().min(1).describe("Order quantity as a decimal string"),
@@ -595,7 +713,7 @@ function registerWriteTools(server, client, keyType, defaultPortfolioId) {
595
713
  server.tool("quicktrade", "Execute a QuickTrade market swap immediately. Only use when the user explicitly requests a QuickTrade. This is irreversible — funds move on-chain once submitted. Call get_quicktrade_quote first and confirm details with the user before executing, unless the user requests to skip the quote step.", {
596
714
  target_asset: z4.string().min(1).describe("Target asset contract address"),
597
715
  contra_asset: z4.string().min(1).describe("Contra (quote) asset contract address"),
598
- chain: ChainEnum.describe("Blockchain network name"),
716
+ chain: chainParam("Blockchain network name"),
599
717
  qty: z4.string().min(1).describe("Order quantity as a decimal string"),
600
718
  order_side: OrderSideEnum.describe("Buy or sell"),
601
719
  slippage_tolerance: z4.string().optional().describe("Slippage tolerance as a decimal string (e.g. '0.01' for 1%)"),
@@ -636,8 +754,8 @@ function registerWriteTools(server, client, keyType, defaultPortfolioId) {
636
754
  return jsonResult(result);
637
755
  });
638
756
  server.tool("bridge_submit", "Execute a cross-chain bridge transfer. This is irreversible — funds are sent from the source chain to the destination chain. Always call bridge_quote first and confirm the details with the user before executing.", {
639
- from_chain: ChainEnum.describe("Source blockchain network"),
640
- to_chain: ChainEnum.describe("Destination blockchain network"),
757
+ from_chain: chainParam("Source blockchain network"),
758
+ to_chain: chainParam("Destination blockchain network"),
641
759
  from_asset_address: z4.string().min(1).describe("Source asset contract address"),
642
760
  to_asset_address: z4.string().min(1).describe("Destination asset contract address"),
643
761
  from_amount: z4.string().min(1).describe("Amount to bridge as a decimal string"),
@@ -686,17 +804,23 @@ import { z as z5 } from "zod";
686
804
  var McpEnvSchema = z5.object({
687
805
  DEFINITIVE_API_KEY: z5.string().startsWith("dpka_"),
688
806
  DEFINITIVE_API_SECRET: z5.string().startsWith("dpks_"),
689
- DEFINITIVE_BASE_URL: z5.string().url().default("https://ddp.definitive.fi"),
807
+ DEFINITIVE_BASE_URL: z5.string().url().refine((value) => new URL(value).protocol === "https:", {
808
+ message: "DEFINITIVE_BASE_URL must use https"
809
+ }).default("https://ddp.definitive.fi"),
690
810
  DEFINITIVE_API_KEY_TYPE: z5.enum(["portfolio", "organization"]),
691
811
  DEFINITIVE_PORTFOLIO_ID: z5.string().optional()
692
812
  });
693
813
  var env = McpEnvSchema.parse(process.env);
814
+ if (new URL(env.DEFINITIVE_BASE_URL).host !== "ddp.definitive.fi") {
815
+ console.error(`Warning: DEFINITIVE_BASE_URL points at ${new URL(env.DEFINITIVE_BASE_URL).host}, not ddp.definitive.fi. Signed requests and your API key id are sent there.`);
816
+ }
694
817
  var client = createClient(env.DEFINITIVE_BASE_URL, env.DEFINITIVE_API_KEY, env.DEFINITIVE_API_SECRET);
695
- var server = new McpServer({ name: "Definitive", version: "1.2.0" }, {
696
- instructions: "MCP server for the Definitive on-chain trading platform. Provides unified tools for portfolio management, trading (market/limit/TWAP/stop orders), QuickTrade execution across multiple blockchains, and read-only perps (perpetual futures) market data and account state. Tools auto-route to the correct API based on the configured key type (portfolio or organization). For organization keys, pass portfolio_id to target a specific portfolio, or set DEFINITIVE_PORTFOLIO_ID as default. TRADING: Default to get_trade_quote + submit_trade for trades unless the user specifies QuickTrade."
818
+ var server = new McpServer({ name: "Definitive", version: "1.3.0" }, {
819
+ instructions: "MCP server for the Definitive on-chain trading platform. Provides unified tools for portfolio management, trading (market/limit/TWAP/stop orders), QuickTrade execution across multiple blockchains, and perps (perpetual futures) market data, account state, order previews, and transfers between trading vaults and the perps account. Perps withdrawals need a signature from the perps account owner wallet, which this server cannot produce. Tools auto-route to the correct API based on the configured key type (portfolio or organization). For organization keys, pass portfolio_id to target a specific portfolio, or set DEFINITIVE_PORTFOLIO_ID as default. TRADING: Default to get_trade_quote + submit_trade for trades unless the user specifies QuickTrade."
697
820
  });
698
821
  registerReadTools(server, client, env.DEFINITIVE_API_KEY_TYPE, env.DEFINITIVE_PORTFOLIO_ID);
699
822
  registerWriteTools(server, client, env.DEFINITIVE_API_KEY_TYPE, env.DEFINITIVE_PORTFOLIO_ID);
700
823
  registerPerpsReadTools(server, client, env.DEFINITIVE_API_KEY_TYPE, env.DEFINITIVE_PORTFOLIO_ID);
824
+ registerPerpsWriteTools(server, client, env.DEFINITIVE_API_KEY_TYPE, env.DEFINITIVE_PORTFOLIO_ID);
701
825
  var transport = new StdioServerTransport;
702
826
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@definitive-fi/mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for the Definitive on-chain trading platform",
5
5
  "keywords": [
6
6
  "mcp",