@carbon-copy/mcp 0.2.2 → 0.3.1
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/__tests__/tools.test.js +0 -4
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/client.d.ts +11 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -1
- package/dist/tools/portfolio.d.ts.map +1 -1
- package/dist/tools/portfolio.js +59 -0
- package/dist/tools/portfolio.js.map +1 -1
- package/package.json +6 -2
- package/.github/workflows/publish.yml +0 -25
- package/CLAUDE.md +0 -69
- package/src/__tests__/client.test.ts +0 -427
- package/src/__tests__/resources.test.ts +0 -263
- package/src/__tests__/tools.test.ts +0 -516
- package/src/client.ts +0 -206
- package/src/index.ts +0 -41
- package/src/resources/portfolio.ts +0 -30
- package/src/resources/traders.ts +0 -29
- package/src/tools/account.ts +0 -48
- package/src/tools/orders.ts +0 -88
- package/src/tools/portfolio.ts +0 -106
- package/src/tools/traders.ts +0 -369
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -9
|
@@ -1,516 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { CarbonCopyClient } from "../client.js";
|
|
4
|
-
import { registerAccountTools } from "../tools/account.js";
|
|
5
|
-
import { registerOrderTools } from "../tools/orders.js";
|
|
6
|
-
import { registerPortfolioTools } from "../tools/portfolio.js";
|
|
7
|
-
import { registerTraderTools } from "../tools/traders.js";
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Helpers
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
/** Cast McpServer to access private internals for assertions. */
|
|
14
|
-
type ServerInternals = {
|
|
15
|
-
_registeredTools: Record<
|
|
16
|
-
string,
|
|
17
|
-
{
|
|
18
|
-
title?: string;
|
|
19
|
-
description?: string;
|
|
20
|
-
annotations?: Record<string, unknown>;
|
|
21
|
-
handler: (...args: unknown[]) => Promise<unknown>;
|
|
22
|
-
enabled: boolean;
|
|
23
|
-
}
|
|
24
|
-
>;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function getTools(server: McpServer) {
|
|
28
|
-
return (server as unknown as ServerInternals)._registeredTools;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function createMockFetchResponse(body: unknown = {}) {
|
|
32
|
-
return {
|
|
33
|
-
ok: true,
|
|
34
|
-
status: 200,
|
|
35
|
-
statusText: "OK",
|
|
36
|
-
headers: { get: () => null },
|
|
37
|
-
json: vi.fn().mockResolvedValue(body),
|
|
38
|
-
text: vi.fn().mockResolvedValue(JSON.stringify(body)),
|
|
39
|
-
clone: vi.fn().mockReturnValue({ json: vi.fn().mockResolvedValue(body) }),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Setup
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
const ALL_TOOL_NAMES = [
|
|
48
|
-
"get_portfolio",
|
|
49
|
-
"get_portfolio_history",
|
|
50
|
-
"get_positions",
|
|
51
|
-
"list_traders",
|
|
52
|
-
"discover_traders",
|
|
53
|
-
"follow_trader",
|
|
54
|
-
"get_trader",
|
|
55
|
-
"get_trader_performance",
|
|
56
|
-
"update_trader",
|
|
57
|
-
"batch_update_traders",
|
|
58
|
-
"unfollow_trader",
|
|
59
|
-
"pause_trader",
|
|
60
|
-
"resume_trader",
|
|
61
|
-
"list_orders",
|
|
62
|
-
"get_order",
|
|
63
|
-
"get_account",
|
|
64
|
-
"health",
|
|
65
|
-
] as const;
|
|
66
|
-
|
|
67
|
-
describe("Tool Registration", () => {
|
|
68
|
-
let server: McpServer;
|
|
69
|
-
let client: CarbonCopyClient;
|
|
70
|
-
let fetchMock: ReturnType<typeof vi.fn>;
|
|
71
|
-
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
fetchMock = vi.fn();
|
|
74
|
-
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
75
|
-
|
|
76
|
-
server = new McpServer({ name: "test", version: "0.0.1" });
|
|
77
|
-
client = new CarbonCopyClient("cc_test");
|
|
78
|
-
|
|
79
|
-
registerPortfolioTools(server, client);
|
|
80
|
-
registerTraderTools(server, client);
|
|
81
|
-
registerOrderTools(server, client);
|
|
82
|
-
registerAccountTools(server, client);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
afterEach(() => {
|
|
86
|
-
vi.restoreAllMocks();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// -------------------------------------------------------------------------
|
|
90
|
-
// Count & names
|
|
91
|
-
// -------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
it("registers exactly 17 tools", () => {
|
|
94
|
-
const tools = getTools(server);
|
|
95
|
-
expect(Object.keys(tools)).toHaveLength(17);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("registers all expected tool names", () => {
|
|
99
|
-
const tools = getTools(server);
|
|
100
|
-
for (const name of ALL_TOOL_NAMES) {
|
|
101
|
-
expect(tools).toHaveProperty(name);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("all registered tools are enabled by default", () => {
|
|
106
|
-
const tools = getTools(server);
|
|
107
|
-
for (const tool of Object.values(tools)) {
|
|
108
|
-
expect(tool.enabled).toBe(true);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// -------------------------------------------------------------------------
|
|
113
|
-
// Annotations
|
|
114
|
-
// -------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
describe("tool annotations", () => {
|
|
117
|
-
it("get_portfolio — readOnlyHint:true, no destructiveHint", () => {
|
|
118
|
-
const { annotations } = getTools(server)["get_portfolio"];
|
|
119
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
120
|
-
expect(annotations?.destructiveHint).toBeUndefined();
|
|
121
|
-
expect(annotations?.idempotentHint).toBeUndefined();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("get_portfolio_history — readOnlyHint:true", () => {
|
|
125
|
-
const { annotations } = getTools(server)["get_portfolio_history"];
|
|
126
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("get_positions — readOnlyHint:true", () => {
|
|
130
|
-
const { annotations } = getTools(server)["get_positions"];
|
|
131
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("list_traders — readOnlyHint:true", () => {
|
|
135
|
-
const { annotations } = getTools(server)["list_traders"];
|
|
136
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("discover_traders — readOnlyHint:true", () => {
|
|
140
|
-
const { annotations } = getTools(server)["discover_traders"];
|
|
141
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("follow_trader — readOnlyHint:false, no destructiveHint", () => {
|
|
145
|
-
const { annotations } = getTools(server)["follow_trader"];
|
|
146
|
-
expect(annotations?.readOnlyHint).toBe(false);
|
|
147
|
-
expect(annotations?.destructiveHint).toBeUndefined();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("get_trader — readOnlyHint:true", () => {
|
|
151
|
-
const { annotations } = getTools(server)["get_trader"];
|
|
152
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("get_trader_performance — readOnlyHint:true", () => {
|
|
156
|
-
const { annotations } = getTools(server)["get_trader_performance"];
|
|
157
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("update_trader — readOnlyHint:false, idempotentHint:true", () => {
|
|
161
|
-
const { annotations } = getTools(server)["update_trader"];
|
|
162
|
-
expect(annotations?.readOnlyHint).toBe(false);
|
|
163
|
-
expect(annotations?.idempotentHint).toBe(true);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("batch_update_traders — readOnlyHint:false, idempotentHint:true", () => {
|
|
167
|
-
const { annotations } = getTools(server)["batch_update_traders"];
|
|
168
|
-
expect(annotations?.readOnlyHint).toBe(false);
|
|
169
|
-
expect(annotations?.idempotentHint).toBe(true);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("unfollow_trader — readOnlyHint:false, destructiveHint:true", () => {
|
|
173
|
-
const { annotations } = getTools(server)["unfollow_trader"];
|
|
174
|
-
expect(annotations?.readOnlyHint).toBe(false);
|
|
175
|
-
expect(annotations?.destructiveHint).toBe(true);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("pause_trader — readOnlyHint:false, idempotentHint:true", () => {
|
|
179
|
-
const { annotations } = getTools(server)["pause_trader"];
|
|
180
|
-
expect(annotations?.readOnlyHint).toBe(false);
|
|
181
|
-
expect(annotations?.idempotentHint).toBe(true);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("resume_trader — readOnlyHint:false, idempotentHint:true", () => {
|
|
185
|
-
const { annotations } = getTools(server)["resume_trader"];
|
|
186
|
-
expect(annotations?.readOnlyHint).toBe(false);
|
|
187
|
-
expect(annotations?.idempotentHint).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("list_orders — readOnlyHint:true", () => {
|
|
191
|
-
const { annotations } = getTools(server)["list_orders"];
|
|
192
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("get_order — readOnlyHint:true", () => {
|
|
196
|
-
const { annotations } = getTools(server)["get_order"];
|
|
197
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("get_account — readOnlyHint:true", () => {
|
|
201
|
-
const { annotations } = getTools(server)["get_account"];
|
|
202
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("health — readOnlyHint:true", () => {
|
|
206
|
-
const { annotations } = getTools(server)["health"];
|
|
207
|
-
expect(annotations?.readOnlyHint).toBe(true);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// -------------------------------------------------------------------------
|
|
212
|
-
// Tool execution — verify handlers call client and return content
|
|
213
|
-
// -------------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
describe("tool execution", () => {
|
|
216
|
-
it("get_portfolio — calls getPortfolio() and returns JSON text", async () => {
|
|
217
|
-
const responseData = { totalValue: 1000, pnl: 50 };
|
|
218
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
219
|
-
|
|
220
|
-
const tool = getTools(server)["get_portfolio"];
|
|
221
|
-
const result = (await tool.handler({}, {})) as {
|
|
222
|
-
content: { type: string; text: string }[];
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
expect(result.content).toHaveLength(1);
|
|
226
|
-
expect(result.content[0].type).toBe("text");
|
|
227
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("get_portfolio_history — passes params and returns JSON text", async () => {
|
|
231
|
-
const responseData = { items: [], cursor: null };
|
|
232
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
233
|
-
|
|
234
|
-
const tool = getTools(server)["get_portfolio_history"];
|
|
235
|
-
const result = (await tool.handler({ limit: 10, cursor: "abc" }, {})) as {
|
|
236
|
-
content: { type: string; text: string }[];
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
expect(result.content[0].type).toBe("text");
|
|
240
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
241
|
-
|
|
242
|
-
// Verify the correct URL was fetched
|
|
243
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
244
|
-
expect(url).toContain("/api/v1/portfolio/history");
|
|
245
|
-
expect(url).toContain("limit=10");
|
|
246
|
-
expect(url).toContain("cursor=abc");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("get_positions — calls getPositions() and returns JSON text", async () => {
|
|
250
|
-
const responseData = { positions: [] };
|
|
251
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
252
|
-
|
|
253
|
-
const tool = getTools(server)["get_positions"];
|
|
254
|
-
const result = (await tool.handler({}, {})) as {
|
|
255
|
-
content: { type: string; text: string }[];
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
259
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
260
|
-
expect(url).toContain("/api/v1/portfolio/positions");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("list_traders — calls getTraders() and returns JSON text", async () => {
|
|
264
|
-
const responseData = [{ wallet: "0xabc", copyPercentage: 50 }];
|
|
265
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
266
|
-
|
|
267
|
-
const tool = getTools(server)["list_traders"];
|
|
268
|
-
const result = (await tool.handler({}, {})) as {
|
|
269
|
-
content: { type: string; text: string }[];
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
273
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
274
|
-
expect(url).toContain("/api/v1/portfolio/traders");
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("discover_traders — calls discoverTraders() and returns JSON text", async () => {
|
|
278
|
-
const responseData = [{ walletAddress: "0xabc", roi: 12.5 }];
|
|
279
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
280
|
-
|
|
281
|
-
const tool = getTools(server)["discover_traders"];
|
|
282
|
-
const result = (await tool.handler({ sortBy: "roi" }, {})) as {
|
|
283
|
-
content: { type: string; text: string }[];
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
287
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
288
|
-
expect(url).toMatch(/\/api\/v1\/traders(?:\?|$)/);
|
|
289
|
-
expect(url).toContain("sortBy=roi");
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("follow_trader — posts follow request and returns JSON text", async () => {
|
|
293
|
-
const responseData = { id: "follow-1", wallet: "0xabc" };
|
|
294
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
295
|
-
|
|
296
|
-
const tool = getTools(server)["follow_trader"];
|
|
297
|
-
const args = { wallet: "0xabc", copyPercentage: 50 };
|
|
298
|
-
const result = (await tool.handler(args, {})) as {
|
|
299
|
-
content: { type: string; text: string }[];
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
303
|
-
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
304
|
-
expect(url).toContain("/api/v1/portfolio/traders");
|
|
305
|
-
expect(init.method).toBe("POST");
|
|
306
|
-
expect(JSON.parse(init.body as string)).toMatchObject({
|
|
307
|
-
walletAddress: args.wallet,
|
|
308
|
-
copyPercentage: args.copyPercentage,
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("get_trader — calls getTrader(wallet) and returns JSON text", async () => {
|
|
313
|
-
const responseData = { wallet: "0xabc", copyPercentage: 50 };
|
|
314
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
315
|
-
|
|
316
|
-
const tool = getTools(server)["get_trader"];
|
|
317
|
-
const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
|
|
318
|
-
content: { type: string; text: string }[];
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
322
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
323
|
-
expect(url).toContain("/api/v1/portfolio/traders/0xabc");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("get_trader_performance — calls performance endpoint and returns JSON text", async () => {
|
|
327
|
-
const responseData = { walletAddress: "0xabc", sharpe: 1.2 };
|
|
328
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
329
|
-
|
|
330
|
-
const tool = getTools(server)["get_trader_performance"];
|
|
331
|
-
const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
|
|
332
|
-
content: { type: string; text: string }[];
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
336
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
337
|
-
expect(url).toContain("/api/v1/traders/0xabc/performance");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("update_trader — patches trader settings and returns JSON text", async () => {
|
|
341
|
-
const responseData = { wallet: "0xabc", copyPercentage: 75 };
|
|
342
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
343
|
-
|
|
344
|
-
const tool = getTools(server)["update_trader"];
|
|
345
|
-
const result = (await tool.handler(
|
|
346
|
-
{ wallet: "0xabc", copyPercentage: 75 },
|
|
347
|
-
{},
|
|
348
|
-
)) as { content: { type: string; text: string }[] };
|
|
349
|
-
|
|
350
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
351
|
-
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
352
|
-
expect(url).toContain("/api/v1/portfolio/traders/0xabc");
|
|
353
|
-
expect(init.method).toBe("PATCH");
|
|
354
|
-
expect(JSON.parse(init.body as string)).toEqual({ copyPercentage: 75 });
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("batch_update_traders — patches multiple traders", async () => {
|
|
358
|
-
const responseData = { updated: 2, notFound: 0, total: 2 };
|
|
359
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
360
|
-
|
|
361
|
-
const tool = getTools(server)["batch_update_traders"];
|
|
362
|
-
const updates = [
|
|
363
|
-
{ wallet: "0xabc", copyPercentage: 75 },
|
|
364
|
-
{ wallet: "0xdef", copyPercentage: 50 },
|
|
365
|
-
];
|
|
366
|
-
const result = (await tool.handler({ updates }, {})) as {
|
|
367
|
-
content: { type: string; text: string }[];
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
371
|
-
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
372
|
-
expect(url).toContain("/api/v1/portfolio/traders/batch");
|
|
373
|
-
expect(init.method).toBe("PATCH");
|
|
374
|
-
expect(JSON.parse(init.body as string)).toEqual({
|
|
375
|
-
updates: [
|
|
376
|
-
{ walletAddress: "0xabc", copyPercentage: 75 },
|
|
377
|
-
{ walletAddress: "0xdef", copyPercentage: 50 },
|
|
378
|
-
],
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it("unfollow_trader — deletes trader and returns JSON text (null body)", async () => {
|
|
383
|
-
fetchMock.mockResolvedValue({
|
|
384
|
-
ok: true,
|
|
385
|
-
status: 204,
|
|
386
|
-
statusText: "No Content",
|
|
387
|
-
headers: { get: () => null },
|
|
388
|
-
json: vi.fn(),
|
|
389
|
-
text: vi.fn(),
|
|
390
|
-
clone: vi.fn(),
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const tool = getTools(server)["unfollow_trader"];
|
|
394
|
-
const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
|
|
395
|
-
content: { type: string; text: string }[];
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
expect(result.content[0].type).toBe("text");
|
|
399
|
-
expect(JSON.parse(result.content[0].text)).toEqual({ success: true });
|
|
400
|
-
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
401
|
-
expect(url).toContain("/api/v1/portfolio/traders/0xabc");
|
|
402
|
-
expect(init.method).toBe("DELETE");
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("pause_trader — pauses trader and returns JSON text", async () => {
|
|
406
|
-
const responseData = { paused: true };
|
|
407
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
408
|
-
|
|
409
|
-
const tool = getTools(server)["pause_trader"];
|
|
410
|
-
const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
|
|
411
|
-
content: { type: string; text: string }[];
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
415
|
-
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
416
|
-
expect(url).toContain("/api/v1/portfolio/traders/0xabc/pause");
|
|
417
|
-
expect(init.method).toBe("POST");
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("resume_trader — resumes trader and returns JSON text", async () => {
|
|
421
|
-
const responseData = { paused: false };
|
|
422
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
423
|
-
|
|
424
|
-
const tool = getTools(server)["resume_trader"];
|
|
425
|
-
const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
|
|
426
|
-
content: { type: string; text: string }[];
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
430
|
-
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
431
|
-
expect(url).toContain("/api/v1/portfolio/traders/0xabc/resume");
|
|
432
|
-
expect(init.method).toBe("POST");
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it("list_orders — calls getOrders() with params and returns JSON text", async () => {
|
|
436
|
-
const responseData = { orders: [], cursor: null };
|
|
437
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
438
|
-
|
|
439
|
-
const tool = getTools(server)["list_orders"];
|
|
440
|
-
const result = (await tool.handler({ status: "filled" }, {})) as {
|
|
441
|
-
content: { type: string; text: string }[];
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
445
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
446
|
-
expect(url).toContain("/api/v1/orders");
|
|
447
|
-
expect(url).toContain("status=filled");
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it("get_order — calls getOrder(id) and returns JSON text", async () => {
|
|
451
|
-
const responseData = { id: "id123", status: "filled" };
|
|
452
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
453
|
-
|
|
454
|
-
const tool = getTools(server)["get_order"];
|
|
455
|
-
const result = (await tool.handler({ id: "id123" }, {})) as {
|
|
456
|
-
content: { type: string; text: string }[];
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
460
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
461
|
-
expect(url).toContain("/api/v1/orders/id123");
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it("get_account — calls getAccount() and returns JSON text", async () => {
|
|
465
|
-
const responseData = { wallet: "0xuser", balance: 500 };
|
|
466
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
467
|
-
|
|
468
|
-
const tool = getTools(server)["get_account"];
|
|
469
|
-
const result = (await tool.handler({}, {})) as {
|
|
470
|
-
content: { type: string; text: string }[];
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
474
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
475
|
-
expect(url).toContain("/api/v1/account");
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it("health — calls health() and returns JSON text", async () => {
|
|
479
|
-
const responseData = { status: "ok" };
|
|
480
|
-
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
481
|
-
|
|
482
|
-
const tool = getTools(server)["health"];
|
|
483
|
-
const result = (await tool.handler({}, {})) as {
|
|
484
|
-
content: { type: string; text: string }[];
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
488
|
-
const [url] = fetchMock.mock.calls[0] as [string];
|
|
489
|
-
expect(url).toContain("/api/v1/health");
|
|
490
|
-
});
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// -------------------------------------------------------------------------
|
|
494
|
-
// Error propagation through tools
|
|
495
|
-
// -------------------------------------------------------------------------
|
|
496
|
-
|
|
497
|
-
describe("tool error propagation", () => {
|
|
498
|
-
it("get_portfolio rejects when client throws an API error", async () => {
|
|
499
|
-
fetchMock.mockResolvedValue({
|
|
500
|
-
ok: false,
|
|
501
|
-
status: 403,
|
|
502
|
-
statusText: "Forbidden",
|
|
503
|
-
headers: { get: () => null },
|
|
504
|
-
json: vi.fn().mockResolvedValue({ message: "forbidden" }),
|
|
505
|
-
clone: vi.fn().mockReturnValue({
|
|
506
|
-
json: vi.fn().mockResolvedValue({ message: "forbidden" }),
|
|
507
|
-
}),
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
const tool = getTools(server)["get_portfolio"];
|
|
511
|
-
await expect(tool.handler({}, {})).rejects.toThrow(
|
|
512
|
-
/Carbon Copy API error 403/,
|
|
513
|
-
);
|
|
514
|
-
});
|
|
515
|
-
});
|
|
516
|
-
});
|