@definitive-fi/mcp 1.0.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.
- package/README.md +33 -0
- package/dist/server.js +265 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @definitive-fi/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for the [Definitive](https://definitive.fi) on-chain trading platform.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Add the following to your MCP client configuration (e.g., Claude Desktop, Claude Code, Cursor):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"definitive": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@definitive-fi/mcp@latest"],
|
|
15
|
+
"env": {
|
|
16
|
+
"DEFINITIVE_API_KEY": "dpka_...",
|
|
17
|
+
"DEFINITIVE_API_SECRET": "dpks_...",
|
|
18
|
+
"DEFINITIVE_API_KEY_TYPE": "portfolio"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Environment Variables
|
|
26
|
+
|
|
27
|
+
| Variable | Required | Description |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `DEFINITIVE_API_KEY` | Yes | Your Definitive API key |
|
|
30
|
+
| `DEFINITIVE_API_SECRET` | Yes | Your Definitive API secret |
|
|
31
|
+
| `DEFINITIVE_API_KEY_TYPE` | Yes | Key type: `"portfolio"` or `"organization"` |
|
|
32
|
+
| `DEFINITIVE_BASE_URL` | No | API base URL (defaults to production) |
|
|
33
|
+
| `DEFINITIVE_PORTFOLIO_ID` | No | Portfolio ID (required for organization keys when targeting a specific portfolio) |
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
var PublicAuthHelpers = {
|
|
9
|
+
generateHMAC(key, message) {
|
|
10
|
+
return crypto.createHmac("sha256", key).update(message).digest("hex");
|
|
11
|
+
},
|
|
12
|
+
preparePrehash({
|
|
13
|
+
method,
|
|
14
|
+
timestamp,
|
|
15
|
+
path,
|
|
16
|
+
queryParams,
|
|
17
|
+
headers,
|
|
18
|
+
body
|
|
19
|
+
}) {
|
|
20
|
+
const filtered = Object.entries(headers).filter(([key]) => key.toLowerCase().startsWith("x-definitive-"));
|
|
21
|
+
const sortedHeaders = filtered.sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${JSON.stringify(value)}`).join(",");
|
|
22
|
+
if (filtered.length > 3) {
|
|
23
|
+
throw new Error("Headers are too long - are you adding a new header?");
|
|
24
|
+
}
|
|
25
|
+
const queryParamsString = new URLSearchParams(queryParams).toString();
|
|
26
|
+
const bodyString = body ?? "";
|
|
27
|
+
return `${method}:${path}?${queryParamsString}:${timestamp}:${sortedHeaders}${bodyString}`;
|
|
28
|
+
},
|
|
29
|
+
clientSignMessage(apiSecret, message) {
|
|
30
|
+
const secret = apiSecret.replace("dpks_", "");
|
|
31
|
+
return PublicAuthHelpers.generateHMAC(secret, message);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
function createClient(baseUrl, apiKey, apiSecret) {
|
|
37
|
+
return {
|
|
38
|
+
async request(method, path, queryParams, body) {
|
|
39
|
+
const timestamp = Date.now().toString();
|
|
40
|
+
const headers = {
|
|
41
|
+
"x-definitive-api-key": apiKey,
|
|
42
|
+
"x-definitive-timestamp": timestamp
|
|
43
|
+
};
|
|
44
|
+
const bodyString = body !== undefined ? JSON.stringify(body) : undefined;
|
|
45
|
+
const prehash = PublicAuthHelpers.preparePrehash({
|
|
46
|
+
method,
|
|
47
|
+
path,
|
|
48
|
+
timestamp,
|
|
49
|
+
headers,
|
|
50
|
+
queryParams,
|
|
51
|
+
body: bodyString
|
|
52
|
+
});
|
|
53
|
+
const signature = PublicAuthHelpers.clientSignMessage(apiSecret, prehash);
|
|
54
|
+
const queryString = queryParams ? new URLSearchParams(queryParams).toString() : "";
|
|
55
|
+
const url = queryString ? `${baseUrl}${path}?${queryString}` : `${baseUrl}${path}`;
|
|
56
|
+
const fetchHeaders = {
|
|
57
|
+
...headers,
|
|
58
|
+
"x-definitive-signature": signature
|
|
59
|
+
};
|
|
60
|
+
if (bodyString) {
|
|
61
|
+
fetchHeaders["Content-Type"] = "application/json";
|
|
62
|
+
}
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
method,
|
|
65
|
+
headers: fetchHeaders,
|
|
66
|
+
body: bodyString
|
|
67
|
+
});
|
|
68
|
+
let json;
|
|
69
|
+
try {
|
|
70
|
+
json = await response.json();
|
|
71
|
+
} catch {
|
|
72
|
+
return {
|
|
73
|
+
error: true,
|
|
74
|
+
status_code: response.status,
|
|
75
|
+
message: `Non-JSON response: ${response.statusText}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
return {
|
|
80
|
+
error: true,
|
|
81
|
+
status_code: response.status,
|
|
82
|
+
message: typeof json === "object" && json !== null && "message" in json ? String(json.message) : response.statusText
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return json;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
function resolvePortfolioRoute(keyType, suffix, portfolioId, defaultPortfolioId) {
|
|
92
|
+
if (keyType === "portfolio") {
|
|
93
|
+
return `/v2/portfolio/${suffix}`;
|
|
94
|
+
}
|
|
95
|
+
const id = portfolioId ?? defaultPortfolioId;
|
|
96
|
+
if (!id) {
|
|
97
|
+
throw new Error("portfolio_id is required for organization keys when DEFINITIVE_PORTFOLIO_ID is not set");
|
|
98
|
+
}
|
|
99
|
+
return `/v2/organization/portfolios/${id}/${suffix}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
import { z } from "zod";
|
|
104
|
+
var zUUID = z.string().uuid();
|
|
105
|
+
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
|
+
]);
|
|
120
|
+
var jsonResult = (result) => ({
|
|
121
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
122
|
+
});
|
|
123
|
+
function registerReadTools(server, client, keyType, defaultPortfolioId) {
|
|
124
|
+
const resolve = (suffix, portfolioId) => resolvePortfolioRoute(keyType, suffix, portfolioId, defaultPortfolioId);
|
|
125
|
+
server.tool("list_positions", "List current token holdings in the portfolio with balances, USD values, and P&L.", {
|
|
126
|
+
limit: z.number().min(1).max(100).default(20).describe("Max positions to return (1-100)"),
|
|
127
|
+
cursor: z.string().optional().describe("Pagination cursor from previous response's nextCursor field"),
|
|
128
|
+
include_dust_balances: z.boolean().default(false).describe("Include positions worth less than $1"),
|
|
129
|
+
portfolio_id: portfolioIdParam
|
|
130
|
+
}, async ({ limit, cursor, include_dust_balances, portfolio_id }) => {
|
|
131
|
+
const path = resolve("positions", portfolio_id);
|
|
132
|
+
const params = { limit: String(limit) };
|
|
133
|
+
if (cursor)
|
|
134
|
+
params.cursor = cursor;
|
|
135
|
+
if (include_dust_balances)
|
|
136
|
+
params.includeDustBalances = "true";
|
|
137
|
+
const result = await client.request("GET", path, params);
|
|
138
|
+
return jsonResult(result);
|
|
139
|
+
});
|
|
140
|
+
server.tool("get_portfolio", "Get portfolio details including vaults, addresses, chains, and signers.", {
|
|
141
|
+
portfolio_id: portfolioIdParam
|
|
142
|
+
}, async ({ portfolio_id }) => {
|
|
143
|
+
const path = resolve("", portfolio_id);
|
|
144
|
+
const result = await client.request("GET", path.replace(/\/$/, ""));
|
|
145
|
+
return jsonResult(result);
|
|
146
|
+
});
|
|
147
|
+
server.tool("list_transfers", "List historical transfers (deposits, withdrawals, swaps, bridge transactions) for the portfolio.", {
|
|
148
|
+
limit: z.number().min(1).max(100).default(20).describe("Max transfers to return (1-100)"),
|
|
149
|
+
cursor: z.string().optional().describe("Pagination cursor from previous response's nextCursor field"),
|
|
150
|
+
action_types: z.array(z.enum([
|
|
151
|
+
"VAULT_ACTION_DEPOSIT",
|
|
152
|
+
"VAULT_ACTION_WITHDRAW",
|
|
153
|
+
"VAULT_ACTION_BRIDGE_SEND",
|
|
154
|
+
"VAULT_ACTION_BRIDGE_RECEIVE",
|
|
155
|
+
"VAULT_ACTION_SWAP"
|
|
156
|
+
])).optional().describe("Filter by transfer action types"),
|
|
157
|
+
start_timestamp: z.string().optional().describe("Start of time range (ISO 8601 string)"),
|
|
158
|
+
end_timestamp: z.string().optional().describe("End of time range (ISO 8601 string)"),
|
|
159
|
+
portfolio_id: portfolioIdParam
|
|
160
|
+
}, async ({
|
|
161
|
+
limit,
|
|
162
|
+
cursor,
|
|
163
|
+
action_types,
|
|
164
|
+
start_timestamp,
|
|
165
|
+
end_timestamp,
|
|
166
|
+
portfolio_id
|
|
167
|
+
}) => {
|
|
168
|
+
const path = resolve("transfers", portfolio_id);
|
|
169
|
+
const params = { limit: String(limit) };
|
|
170
|
+
if (cursor)
|
|
171
|
+
params.cursor = cursor;
|
|
172
|
+
if (action_types?.length)
|
|
173
|
+
params.actionTypes = action_types.join(",");
|
|
174
|
+
if (start_timestamp)
|
|
175
|
+
params.startTimestamp = start_timestamp;
|
|
176
|
+
if (end_timestamp)
|
|
177
|
+
params.endTimestamp = end_timestamp;
|
|
178
|
+
const result = await client.request("GET", path, params);
|
|
179
|
+
return jsonResult(result);
|
|
180
|
+
});
|
|
181
|
+
server.tool("get_deposit_address", "Get a deposit address for a specific blockchain. Creates a vault if one doesn't exist for that chain.", {
|
|
182
|
+
chain: ChainEnum.describe("Blockchain network name"),
|
|
183
|
+
wallet_address: z.string().min(1).describe("User's wallet address on this chain — ask the user if not provided"),
|
|
184
|
+
portfolio_id: portfolioIdParam
|
|
185
|
+
}, async ({ chain, wallet_address, portfolio_id }) => {
|
|
186
|
+
const path = resolve(`address/${chain}`, portfolio_id);
|
|
187
|
+
const params = { walletAddress: wallet_address };
|
|
188
|
+
const result = await client.request("GET", path, params);
|
|
189
|
+
return jsonResult(result);
|
|
190
|
+
});
|
|
191
|
+
server.tool("list_orders", "List trade orders with optional status filtering. Returns order details including fill status and pricing.", {
|
|
192
|
+
limit: z.number().min(1).max(100).default(20).describe("Max orders to return (1-100)"),
|
|
193
|
+
cursor: z.string().optional().describe("Pagination cursor from previous response's nextCursor field"),
|
|
194
|
+
status: z.enum([
|
|
195
|
+
"ORDER_STATUS_PENDING",
|
|
196
|
+
"ORDER_STATUS_ACCEPTED",
|
|
197
|
+
"ORDER_STATUS_PARTIALLY_FILLED",
|
|
198
|
+
"ORDER_STATUS_FILLED",
|
|
199
|
+
"ORDER_STATUS_CANCELLED"
|
|
200
|
+
]).optional().describe("Filter orders by status"),
|
|
201
|
+
portfolio_id: portfolioIdParam
|
|
202
|
+
}, async ({ limit, cursor, status, portfolio_id }) => {
|
|
203
|
+
const path = resolve("orders", portfolio_id);
|
|
204
|
+
const params = { limit: String(limit) };
|
|
205
|
+
if (cursor)
|
|
206
|
+
params.cursor = cursor;
|
|
207
|
+
if (status)
|
|
208
|
+
params.filters = JSON.stringify({ status });
|
|
209
|
+
const result = await client.request("GET", path, params);
|
|
210
|
+
return jsonResult(result);
|
|
211
|
+
});
|
|
212
|
+
server.tool("get_order_details", "Get detailed information about a specific order including individual fill history.", {
|
|
213
|
+
order_id: zUUID.describe("The order UUID to look up"),
|
|
214
|
+
portfolio_id: portfolioIdParam
|
|
215
|
+
}, async ({ order_id, portfolio_id }) => {
|
|
216
|
+
const path = resolve(`orders/${order_id}`, portfolio_id);
|
|
217
|
+
const result = await client.request("GET", path);
|
|
218
|
+
return jsonResult(result);
|
|
219
|
+
});
|
|
220
|
+
if (keyType !== "organization")
|
|
221
|
+
return;
|
|
222
|
+
server.tool("list_portfolios", "List all portfolios in the organization. Organization API keys only.", {
|
|
223
|
+
limit: z.number().min(1).max(100).default(10).describe("Max portfolios to return (1-100)"),
|
|
224
|
+
cursor: z.string().optional().describe("Pagination cursor from previous response's nextCursor field")
|
|
225
|
+
}, async ({ limit, cursor }) => {
|
|
226
|
+
const params = { limit: String(limit) };
|
|
227
|
+
if (cursor)
|
|
228
|
+
params.cursor = cursor;
|
|
229
|
+
const result = await client.request("GET", "/v2/organization", params);
|
|
230
|
+
return jsonResult(result);
|
|
231
|
+
});
|
|
232
|
+
server.tool("get_fees", "Get fee summary for a portfolio over a time period. Organization API keys only.", {
|
|
233
|
+
portfolio_id: zUUID.describe("Portfolio UUID to get fees for"),
|
|
234
|
+
start_timestamp: z.string().optional().describe("Start of time range (ISO 8601 string)"),
|
|
235
|
+
end_timestamp: z.string().optional().describe("End of time range (ISO 8601 string)")
|
|
236
|
+
}, async ({ portfolio_id, start_timestamp, end_timestamp }) => {
|
|
237
|
+
const params = {};
|
|
238
|
+
if (start_timestamp)
|
|
239
|
+
params.startTimestamp = start_timestamp;
|
|
240
|
+
if (end_timestamp)
|
|
241
|
+
params.endTimestamp = end_timestamp;
|
|
242
|
+
const result = await client.request("GET", `/v2/organization/portfolios/${portfolio_id}/fees`, params);
|
|
243
|
+
return jsonResult(result);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
import { z as z2 } from "zod";
|
|
249
|
+
var McpEnvSchema = z2.object({
|
|
250
|
+
DEFINITIVE_API_KEY: z2.string().startsWith("dpka_"),
|
|
251
|
+
DEFINITIVE_API_SECRET: z2.string().startsWith("dpks_"),
|
|
252
|
+
DEFINITIVE_BASE_URL: z2.string().url().default("https://ddp.definitive.fi"),
|
|
253
|
+
DEFINITIVE_API_KEY_TYPE: z2.enum(["portfolio", "organization"]),
|
|
254
|
+
DEFINITIVE_PORTFOLIO_ID: z2.string().optional()
|
|
255
|
+
});
|
|
256
|
+
var env = McpEnvSchema.parse(process.env);
|
|
257
|
+
var client = createClient(env.DEFINITIVE_BASE_URL, env.DEFINITIVE_API_KEY, env.DEFINITIVE_API_SECRET);
|
|
258
|
+
var server = new McpServer({
|
|
259
|
+
name: "Definitive",
|
|
260
|
+
version: "1.0.0",
|
|
261
|
+
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 11 blockchains. 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. IMPORTANT: When specifying asset addresses for EVM native assets (ETH, MATIC, AVAX, BNB, HYPE, etc.), use the zero address: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
|
262
|
+
});
|
|
263
|
+
registerReadTools(server, client, env.DEFINITIVE_API_KEY_TYPE, env.DEFINITIVE_PORTFOLIO_ID);
|
|
264
|
+
var transport = new StdioServerTransport;
|
|
265
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@definitive-fi/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Definitive on-chain trading platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "dist/server.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "bun run build.ts"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
18
|
+
"zod": "^3.25.0"
|
|
19
|
+
}
|
|
20
|
+
}
|