@carbon-copy/mcp 0.1.0 → 0.2.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/.github/workflows/publish.yml +3 -1
- package/README.md +48 -22
- package/dist/__tests__/client.test.js +56 -26
- package/dist/__tests__/client.test.js.map +1 -1
- package/dist/__tests__/resources.test.js +3 -3
- package/dist/__tests__/resources.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +72 -14
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/client.d.ts +17 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +24 -8
- package/dist/client.js.map +1 -1
- package/dist/tools/orders.d.ts +2 -2
- package/dist/tools/orders.d.ts.map +1 -1
- package/dist/tools/orders.js +13 -3
- package/dist/tools/orders.js.map +1 -1
- package/dist/tools/traders.d.ts +2 -2
- package/dist/tools/traders.d.ts.map +1 -1
- package/dist/tools/traders.js +146 -8
- package/dist/tools/traders.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/client.test.ts +78 -43
- package/src/__tests__/resources.test.ts +10 -10
- package/src/__tests__/tools.test.ts +92 -16
- package/src/client.ts +60 -15
- package/src/tools/orders.ts +24 -10
- package/src/tools/traders.ts +183 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { CarbonCopyClient } from "../client.js";
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
@@ -31,9 +31,9 @@ function createMockResponse(opts: MockResponseOptions = {}) {
|
|
|
31
31
|
if (contentLength !== undefined) headers["content-length"] = contentLength;
|
|
32
32
|
|
|
33
33
|
const jsonFn = vi.fn().mockResolvedValue(body);
|
|
34
|
-
const textFn = vi
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const textFn = vi
|
|
35
|
+
.fn()
|
|
36
|
+
.mockResolvedValue(bodyText || (body !== null ? JSON.stringify(body) : ""));
|
|
37
37
|
|
|
38
38
|
// The clone is only used for error-path json parsing
|
|
39
39
|
const cloneJsonFn = cloneJsonThrows
|
|
@@ -53,7 +53,7 @@ function createMockResponse(opts: MockResponseOptions = {}) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const BASE = "https://carboncopy.
|
|
56
|
+
const BASE = "https://carboncopy.inc";
|
|
57
57
|
|
|
58
58
|
describe("CarbonCopyClient", () => {
|
|
59
59
|
let fetchMock: ReturnType<typeof vi.fn>;
|
|
@@ -84,10 +84,10 @@ describe("CarbonCopyClient", () => {
|
|
|
84
84
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
85
85
|
expect(url).toBe(`${BASE}/api/v1/portfolio`);
|
|
86
86
|
expect((init.headers as Record<string, string>)["Authorization"]).toBe(
|
|
87
|
-
"Bearer cc_testkey"
|
|
87
|
+
"Bearer cc_testkey",
|
|
88
88
|
);
|
|
89
89
|
expect((init.headers as Record<string, string>)["Content-Type"]).toBe(
|
|
90
|
-
"application/json"
|
|
90
|
+
"application/json",
|
|
91
91
|
);
|
|
92
92
|
expect(init.method).toBe("GET");
|
|
93
93
|
expect(init.body).toBeUndefined();
|
|
@@ -101,14 +101,19 @@ describe("CarbonCopyClient", () => {
|
|
|
101
101
|
fetchMock.mockResolvedValue(createMockResponse({ body: responseData }));
|
|
102
102
|
|
|
103
103
|
const payload = {
|
|
104
|
-
|
|
104
|
+
wallet: "0xabc",
|
|
105
105
|
copyPercentage: 50,
|
|
106
106
|
};
|
|
107
107
|
await client.followTrader(payload);
|
|
108
108
|
|
|
109
109
|
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
110
110
|
expect(init.method).toBe("POST");
|
|
111
|
-
expect(init.body).toBe(
|
|
111
|
+
expect(init.body).toBe(
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
walletAddress: payload.wallet,
|
|
114
|
+
copyPercentage: payload.copyPercentage,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
112
117
|
});
|
|
113
118
|
});
|
|
114
119
|
|
|
@@ -121,15 +126,13 @@ describe("CarbonCopyClient", () => {
|
|
|
121
126
|
status: 401,
|
|
122
127
|
statusText: "Unauthorized",
|
|
123
128
|
body: errBody,
|
|
124
|
-
})
|
|
129
|
+
}),
|
|
125
130
|
);
|
|
126
131
|
|
|
127
132
|
await expect(client.getPortfolio()).rejects.toThrow(
|
|
128
|
-
/Carbon Copy API error 401 Unauthorized
|
|
129
|
-
);
|
|
130
|
-
await expect(client.getPortfolio()).rejects.toThrow(
|
|
131
|
-
/Unauthorized/
|
|
133
|
+
/Carbon Copy API error 401 Unauthorized/,
|
|
132
134
|
);
|
|
135
|
+
await expect(client.getPortfolio()).rejects.toThrow(/Unauthorized/);
|
|
133
136
|
});
|
|
134
137
|
|
|
135
138
|
it("falls back to res.text() when cloned.json() fails on error response", async () => {
|
|
@@ -140,12 +143,10 @@ describe("CarbonCopyClient", () => {
|
|
|
140
143
|
statusText: "Internal Server Error",
|
|
141
144
|
bodyText: "raw error text",
|
|
142
145
|
cloneJsonThrows: true,
|
|
143
|
-
})
|
|
146
|
+
}),
|
|
144
147
|
);
|
|
145
148
|
|
|
146
|
-
await expect(client.getPortfolio()).rejects.toThrow(
|
|
147
|
-
/raw error text/
|
|
148
|
-
);
|
|
149
|
+
await expect(client.getPortfolio()).rejects.toThrow(/raw error text/);
|
|
149
150
|
});
|
|
150
151
|
});
|
|
151
152
|
|
|
@@ -224,9 +225,7 @@ describe("CarbonCopyClient", () => {
|
|
|
224
225
|
it("getPortfolioHistory({limit:10, cursor:'abc'}) → GET /api/v1/portfolio/history?limit=10&cursor=abc", async () => {
|
|
225
226
|
await client.getPortfolioHistory({ limit: 10, cursor: "abc" });
|
|
226
227
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
227
|
-
expect(url).toBe(
|
|
228
|
-
`${BASE}/api/v1/portfolio/history?limit=10&cursor=abc`
|
|
229
|
-
);
|
|
228
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/history?limit=10&cursor=abc`);
|
|
230
229
|
expect(init.method).toBe("GET");
|
|
231
230
|
});
|
|
232
231
|
|
|
@@ -237,65 +236,95 @@ describe("CarbonCopyClient", () => {
|
|
|
237
236
|
expect(init.method).toBe("GET");
|
|
238
237
|
});
|
|
239
238
|
|
|
240
|
-
it("getTraders() → GET /api/v1/traders", async () => {
|
|
239
|
+
it("getTraders() → GET /api/v1/portfolio/traders", async () => {
|
|
241
240
|
await client.getTraders();
|
|
242
241
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
243
|
-
expect(url).toBe(`${BASE}/api/v1/traders`);
|
|
242
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders`);
|
|
244
243
|
expect(init.method).toBe("GET");
|
|
245
244
|
});
|
|
246
245
|
|
|
247
|
-
it("
|
|
246
|
+
it("discoverTraders({sortBy:'roi'}) → GET /api/v1/traders?sortBy=roi", async () => {
|
|
247
|
+
await client.discoverTraders({ sortBy: "roi" });
|
|
248
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
249
|
+
expect(url).toBe(`${BASE}/api/v1/traders?sortBy=roi`);
|
|
250
|
+
expect(init.method).toBe("GET");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("followTrader({...}) → POST /api/v1/portfolio/traders with body", async () => {
|
|
248
254
|
const body = {
|
|
249
|
-
|
|
255
|
+
wallet: "0xabc",
|
|
250
256
|
copyPercentage: 50,
|
|
251
257
|
maxCopyAmount: 100,
|
|
252
258
|
notificationsEnabled: true,
|
|
253
259
|
};
|
|
254
260
|
await client.followTrader(body);
|
|
255
261
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
256
|
-
expect(url).toBe(`${BASE}/api/v1/traders`);
|
|
262
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders`);
|
|
257
263
|
expect(init.method).toBe("POST");
|
|
258
|
-
expect(JSON.parse(init.body as string)).toEqual(
|
|
264
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
265
|
+
walletAddress: body.wallet,
|
|
266
|
+
copyPercentage: body.copyPercentage,
|
|
267
|
+
maxCopyAmount: body.maxCopyAmount,
|
|
268
|
+
notificationsEnabled: body.notificationsEnabled,
|
|
269
|
+
});
|
|
259
270
|
});
|
|
260
271
|
|
|
261
|
-
it("getTrader('0xabc') → GET /api/v1/traders/0xabc", async () => {
|
|
272
|
+
it("getTrader('0xabc') → GET /api/v1/portfolio/traders/0xabc", async () => {
|
|
262
273
|
await client.getTrader("0xabc");
|
|
263
274
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
264
|
-
expect(url).toBe(`${BASE}/api/v1/traders/0xabc`);
|
|
275
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders/0xabc`);
|
|
265
276
|
expect(init.method).toBe("GET");
|
|
266
277
|
});
|
|
267
278
|
|
|
268
|
-
it("updateTrader('0xabc', {...}) → PATCH /api/v1/traders/0xabc with body", async () => {
|
|
279
|
+
it("updateTrader('0xabc', {...}) → PATCH /api/v1/portfolio/traders/0xabc with body", async () => {
|
|
269
280
|
const body = { copyPercentage: 75, copyTradingEnabled: false };
|
|
270
281
|
await client.updateTrader("0xabc", body);
|
|
271
282
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
272
|
-
expect(url).toBe(`${BASE}/api/v1/traders/0xabc`);
|
|
283
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders/0xabc`);
|
|
273
284
|
expect(init.method).toBe("PATCH");
|
|
274
285
|
expect(JSON.parse(init.body as string)).toEqual(body);
|
|
275
286
|
});
|
|
276
287
|
|
|
277
|
-
it("unfollowTrader('0xabc') → DELETE /api/v1/traders/0xabc", async () => {
|
|
278
|
-
fetchMock.mockResolvedValue(
|
|
288
|
+
it("unfollowTrader('0xabc') → DELETE /api/v1/portfolio/traders/0xabc", async () => {
|
|
289
|
+
fetchMock.mockResolvedValue(
|
|
290
|
+
createMockResponse({ status: 204, ok: true }),
|
|
291
|
+
);
|
|
279
292
|
await client.unfollowTrader("0xabc");
|
|
280
293
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
281
|
-
expect(url).toBe(`${BASE}/api/v1/traders/0xabc`);
|
|
294
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders/0xabc`);
|
|
282
295
|
expect(init.method).toBe("DELETE");
|
|
283
296
|
});
|
|
284
297
|
|
|
285
|
-
it("pauseTrader('0xabc') → POST /api/v1/traders/0xabc/pause", async () => {
|
|
298
|
+
it("pauseTrader('0xabc') → POST /api/v1/portfolio/traders/0xabc/pause", async () => {
|
|
286
299
|
await client.pauseTrader("0xabc");
|
|
287
300
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
288
|
-
expect(url).toBe(`${BASE}/api/v1/traders/0xabc/pause`);
|
|
301
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders/0xabc/pause`);
|
|
289
302
|
expect(init.method).toBe("POST");
|
|
290
303
|
});
|
|
291
304
|
|
|
292
|
-
it("resumeTrader('0xabc') → POST /api/v1/traders/0xabc/resume", async () => {
|
|
305
|
+
it("resumeTrader('0xabc') → POST /api/v1/portfolio/traders/0xabc/resume", async () => {
|
|
293
306
|
await client.resumeTrader("0xabc");
|
|
294
307
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
295
|
-
expect(url).toBe(`${BASE}/api/v1/traders/0xabc/resume`);
|
|
308
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders/0xabc/resume`);
|
|
296
309
|
expect(init.method).toBe("POST");
|
|
297
310
|
});
|
|
298
311
|
|
|
312
|
+
it("getTraderPerformance('0xabc') → GET /api/v1/traders/0xabc/performance", async () => {
|
|
313
|
+
await client.getTraderPerformance("0xabc");
|
|
314
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
315
|
+
expect(url).toBe(`${BASE}/api/v1/traders/0xabc/performance`);
|
|
316
|
+
expect(init.method).toBe("GET");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("batchUpdateTraders([...]) → PATCH /api/v1/portfolio/traders/batch", async () => {
|
|
320
|
+
const updates = [{ walletAddress: "0xabc", copyPercentage: 70 }];
|
|
321
|
+
await client.batchUpdateTraders(updates);
|
|
322
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
323
|
+
expect(url).toBe(`${BASE}/api/v1/portfolio/traders/batch`);
|
|
324
|
+
expect(init.method).toBe("PATCH");
|
|
325
|
+
expect(JSON.parse(init.body as string)).toEqual({ updates });
|
|
326
|
+
});
|
|
327
|
+
|
|
299
328
|
it("getOrders({status:'filled'}) → GET /api/v1/orders?status=filled", async () => {
|
|
300
329
|
await client.getOrders({ status: "filled" });
|
|
301
330
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
@@ -354,15 +383,21 @@ describe("CarbonCopyClient", () => {
|
|
|
354
383
|
await client.updateTrader(wallet, { copyPercentage: 10 });
|
|
355
384
|
const [url] = fetchMock.mock.calls[0] as [string];
|
|
356
385
|
expect(url).toContain(encodeURIComponent(wallet));
|
|
357
|
-
expect(url).toBe(
|
|
386
|
+
expect(url).toBe(
|
|
387
|
+
`${BASE}/api/v1/portfolio/traders/${encodeURIComponent(wallet)}`,
|
|
388
|
+
);
|
|
358
389
|
});
|
|
359
390
|
|
|
360
391
|
it("encodes wallet addresses for unfollowTrader", async () => {
|
|
361
392
|
const wallet = "0xabc def";
|
|
362
|
-
fetchMock.mockResolvedValue(
|
|
393
|
+
fetchMock.mockResolvedValue(
|
|
394
|
+
createMockResponse({ status: 204, ok: true }),
|
|
395
|
+
);
|
|
363
396
|
await client.unfollowTrader(wallet);
|
|
364
397
|
const [url] = fetchMock.mock.calls[0] as [string];
|
|
365
|
-
expect(url).toBe(
|
|
398
|
+
expect(url).toBe(
|
|
399
|
+
`${BASE}/api/v1/portfolio/traders/${encodeURIComponent(wallet)}`,
|
|
400
|
+
);
|
|
366
401
|
});
|
|
367
402
|
|
|
368
403
|
it("encodes wallet addresses for pauseTrader", async () => {
|
|
@@ -370,7 +405,7 @@ describe("CarbonCopyClient", () => {
|
|
|
370
405
|
await client.pauseTrader(wallet);
|
|
371
406
|
const [url] = fetchMock.mock.calls[0] as [string];
|
|
372
407
|
expect(url).toBe(
|
|
373
|
-
`${BASE}/api/v1/traders/${encodeURIComponent(wallet)}/pause
|
|
408
|
+
`${BASE}/api/v1/portfolio/traders/${encodeURIComponent(wallet)}/pause`,
|
|
374
409
|
);
|
|
375
410
|
});
|
|
376
411
|
|
|
@@ -379,7 +414,7 @@ describe("CarbonCopyClient", () => {
|
|
|
379
414
|
await client.resumeTrader(wallet);
|
|
380
415
|
const [url] = fetchMock.mock.calls[0] as [string];
|
|
381
416
|
expect(url).toBe(
|
|
382
|
-
`${BASE}/api/v1/traders/${encodeURIComponent(wallet)}/resume
|
|
417
|
+
`${BASE}/api/v1/portfolio/traders/${encodeURIComponent(wallet)}/resume`,
|
|
383
418
|
);
|
|
384
419
|
});
|
|
385
420
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { CarbonCopyClient } from "../client.js";
|
|
4
4
|
import { registerPortfolioResources } from "../resources/portfolio.js";
|
|
5
5
|
import { registerTraderResources } from "../resources/traders.js";
|
|
@@ -137,7 +137,7 @@ describe("Resource Registration", () => {
|
|
|
137
137
|
const resource = getResources(server)["carboncopy://portfolio"];
|
|
138
138
|
const result = (await resource.readCallback(
|
|
139
139
|
new URL("carboncopy://portfolio"),
|
|
140
|
-
{}
|
|
140
|
+
{},
|
|
141
141
|
)) as { contents: { uri: string; mimeType: string; text: string }[] };
|
|
142
142
|
|
|
143
143
|
expect(result.contents[0].mimeType).toBe("application/json");
|
|
@@ -150,7 +150,7 @@ describe("Resource Registration", () => {
|
|
|
150
150
|
const resource = getResources(server)["carboncopy://portfolio"];
|
|
151
151
|
const result = (await resource.readCallback(
|
|
152
152
|
new URL("carboncopy://portfolio"),
|
|
153
|
-
{}
|
|
153
|
+
{},
|
|
154
154
|
)) as { contents: { uri: string; mimeType: string; text: string }[] };
|
|
155
155
|
|
|
156
156
|
expect(JSON.parse(result.contents[0].text)).toEqual(portfolioData);
|
|
@@ -180,7 +180,7 @@ describe("Resource Registration", () => {
|
|
|
180
180
|
|
|
181
181
|
const resource = getResources(server)["carboncopy://portfolio"];
|
|
182
182
|
await expect(
|
|
183
|
-
resource.readCallback(new URL("carboncopy://portfolio"), {})
|
|
183
|
+
resource.readCallback(new URL("carboncopy://portfolio"), {}),
|
|
184
184
|
).rejects.toThrow(/Carbon Copy API error 401/);
|
|
185
185
|
});
|
|
186
186
|
});
|
|
@@ -197,7 +197,7 @@ describe("Resource Registration", () => {
|
|
|
197
197
|
const resource = getResources(server)["carboncopy://traders"];
|
|
198
198
|
const result = (await resource.readCallback(
|
|
199
199
|
new URL("carboncopy://traders"),
|
|
200
|
-
{}
|
|
200
|
+
{},
|
|
201
201
|
)) as { contents: { uri: string; mimeType: string; text: string }[] };
|
|
202
202
|
|
|
203
203
|
expect(result.contents).toHaveLength(1);
|
|
@@ -210,7 +210,7 @@ describe("Resource Registration", () => {
|
|
|
210
210
|
const resource = getResources(server)["carboncopy://traders"];
|
|
211
211
|
const result = (await resource.readCallback(
|
|
212
212
|
new URL("carboncopy://traders"),
|
|
213
|
-
{}
|
|
213
|
+
{},
|
|
214
214
|
)) as { contents: { uri: string; mimeType: string; text: string }[] };
|
|
215
215
|
|
|
216
216
|
expect(result.contents[0].mimeType).toBe("application/json");
|
|
@@ -226,20 +226,20 @@ describe("Resource Registration", () => {
|
|
|
226
226
|
const resource = getResources(server)["carboncopy://traders"];
|
|
227
227
|
const result = (await resource.readCallback(
|
|
228
228
|
new URL("carboncopy://traders"),
|
|
229
|
-
{}
|
|
229
|
+
{},
|
|
230
230
|
)) as { contents: { uri: string; mimeType: string; text: string }[] };
|
|
231
231
|
|
|
232
232
|
expect(JSON.parse(result.contents[0].text)).toEqual(tradersData);
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
it("calls GET /api/v1/traders when traders resource is read", async () => {
|
|
235
|
+
it("calls GET /api/v1/portfolio/traders when traders resource is read", async () => {
|
|
236
236
|
fetchMock.mockResolvedValue(createMockFetchResponse([]));
|
|
237
237
|
|
|
238
238
|
const resource = getResources(server)["carboncopy://traders"];
|
|
239
239
|
await resource.readCallback(new URL("carboncopy://traders"), {});
|
|
240
240
|
|
|
241
241
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
242
|
-
expect(url).toContain("/api/v1/traders");
|
|
242
|
+
expect(url).toContain("/api/v1/portfolio/traders");
|
|
243
243
|
expect(init.method).toBe("GET");
|
|
244
244
|
});
|
|
245
245
|
|
|
@@ -256,7 +256,7 @@ describe("Resource Registration", () => {
|
|
|
256
256
|
|
|
257
257
|
const resource = getResources(server)["carboncopy://traders"];
|
|
258
258
|
await expect(
|
|
259
|
-
resource.readCallback(new URL("carboncopy://traders"), {})
|
|
259
|
+
resource.readCallback(new URL("carboncopy://traders"), {}),
|
|
260
260
|
).rejects.toThrow(/Carbon Copy API error 403/);
|
|
261
261
|
});
|
|
262
262
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { CarbonCopyClient } from "../client.js";
|
|
4
|
+
import { registerAccountTools } from "../tools/account.js";
|
|
5
|
+
import { registerOrderTools } from "../tools/orders.js";
|
|
4
6
|
import { registerPortfolioTools } from "../tools/portfolio.js";
|
|
5
7
|
import { registerTraderTools } from "../tools/traders.js";
|
|
6
|
-
import { registerOrderTools } from "../tools/orders.js";
|
|
7
|
-
import { registerAccountTools } from "../tools/account.js";
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Helpers
|
|
@@ -49,9 +49,12 @@ const ALL_TOOL_NAMES = [
|
|
|
49
49
|
"get_portfolio_history",
|
|
50
50
|
"get_positions",
|
|
51
51
|
"list_traders",
|
|
52
|
+
"discover_traders",
|
|
52
53
|
"follow_trader",
|
|
53
54
|
"get_trader",
|
|
55
|
+
"get_trader_performance",
|
|
54
56
|
"update_trader",
|
|
57
|
+
"batch_update_traders",
|
|
55
58
|
"unfollow_trader",
|
|
56
59
|
"pause_trader",
|
|
57
60
|
"resume_trader",
|
|
@@ -87,9 +90,9 @@ describe("Tool Registration", () => {
|
|
|
87
90
|
// Count & names
|
|
88
91
|
// -------------------------------------------------------------------------
|
|
89
92
|
|
|
90
|
-
it("registers exactly
|
|
93
|
+
it("registers exactly 17 tools", () => {
|
|
91
94
|
const tools = getTools(server);
|
|
92
|
-
expect(Object.keys(tools)).toHaveLength(
|
|
95
|
+
expect(Object.keys(tools)).toHaveLength(17);
|
|
93
96
|
});
|
|
94
97
|
|
|
95
98
|
it("registers all expected tool names", () => {
|
|
@@ -133,6 +136,11 @@ describe("Tool Registration", () => {
|
|
|
133
136
|
expect(annotations?.readOnlyHint).toBe(true);
|
|
134
137
|
});
|
|
135
138
|
|
|
139
|
+
it("discover_traders — readOnlyHint:true", () => {
|
|
140
|
+
const { annotations } = getTools(server)["discover_traders"];
|
|
141
|
+
expect(annotations?.readOnlyHint).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
136
144
|
it("follow_trader — readOnlyHint:false, no destructiveHint", () => {
|
|
137
145
|
const { annotations } = getTools(server)["follow_trader"];
|
|
138
146
|
expect(annotations?.readOnlyHint).toBe(false);
|
|
@@ -144,12 +152,23 @@ describe("Tool Registration", () => {
|
|
|
144
152
|
expect(annotations?.readOnlyHint).toBe(true);
|
|
145
153
|
});
|
|
146
154
|
|
|
155
|
+
it("get_trader_performance — readOnlyHint:true", () => {
|
|
156
|
+
const { annotations } = getTools(server)["get_trader_performance"];
|
|
157
|
+
expect(annotations?.readOnlyHint).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
147
160
|
it("update_trader — readOnlyHint:false, idempotentHint:true", () => {
|
|
148
161
|
const { annotations } = getTools(server)["update_trader"];
|
|
149
162
|
expect(annotations?.readOnlyHint).toBe(false);
|
|
150
163
|
expect(annotations?.idempotentHint).toBe(true);
|
|
151
164
|
});
|
|
152
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
|
+
|
|
153
172
|
it("unfollow_trader — readOnlyHint:false, destructiveHint:true", () => {
|
|
154
173
|
const { annotations } = getTools(server)["unfollow_trader"];
|
|
155
174
|
expect(annotations?.readOnlyHint).toBe(false);
|
|
@@ -252,7 +271,22 @@ describe("Tool Registration", () => {
|
|
|
252
271
|
|
|
253
272
|
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
254
273
|
const [url] = fetchMock.mock.calls[0] as [string];
|
|
255
|
-
expect(url).toContain("/api/v1/traders");
|
|
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");
|
|
256
290
|
});
|
|
257
291
|
|
|
258
292
|
it("follow_trader — posts follow request and returns JSON text", async () => {
|
|
@@ -260,16 +294,19 @@ describe("Tool Registration", () => {
|
|
|
260
294
|
fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
|
|
261
295
|
|
|
262
296
|
const tool = getTools(server)["follow_trader"];
|
|
263
|
-
const args = {
|
|
297
|
+
const args = { wallet: "0xabc", copyPercentage: 50 };
|
|
264
298
|
const result = (await tool.handler(args, {})) as {
|
|
265
299
|
content: { type: string; text: string }[];
|
|
266
300
|
};
|
|
267
301
|
|
|
268
302
|
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
269
303
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
270
|
-
expect(url).toContain("/api/v1/traders");
|
|
304
|
+
expect(url).toContain("/api/v1/portfolio/traders");
|
|
271
305
|
expect(init.method).toBe("POST");
|
|
272
|
-
expect(JSON.parse(init.body as string)).toMatchObject(
|
|
306
|
+
expect(JSON.parse(init.body as string)).toMatchObject({
|
|
307
|
+
walletAddress: args.wallet,
|
|
308
|
+
copyPercentage: args.copyPercentage,
|
|
309
|
+
});
|
|
273
310
|
});
|
|
274
311
|
|
|
275
312
|
it("get_trader — calls getTrader(wallet) and returns JSON text", async () => {
|
|
@@ -283,7 +320,21 @@ describe("Tool Registration", () => {
|
|
|
283
320
|
|
|
284
321
|
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
285
322
|
const [url] = fetchMock.mock.calls[0] as [string];
|
|
286
|
-
expect(url).toContain("/api/v1/traders/0xabc");
|
|
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");
|
|
287
338
|
});
|
|
288
339
|
|
|
289
340
|
it("update_trader — patches trader settings and returns JSON text", async () => {
|
|
@@ -293,16 +344,41 @@ describe("Tool Registration", () => {
|
|
|
293
344
|
const tool = getTools(server)["update_trader"];
|
|
294
345
|
const result = (await tool.handler(
|
|
295
346
|
{ wallet: "0xabc", copyPercentage: 75 },
|
|
296
|
-
{}
|
|
347
|
+
{},
|
|
297
348
|
)) as { content: { type: string; text: string }[] };
|
|
298
349
|
|
|
299
350
|
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
300
351
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
301
|
-
expect(url).toContain("/api/v1/traders/0xabc");
|
|
352
|
+
expect(url).toContain("/api/v1/portfolio/traders/0xabc");
|
|
302
353
|
expect(init.method).toBe("PATCH");
|
|
303
354
|
expect(JSON.parse(init.body as string)).toEqual({ copyPercentage: 75 });
|
|
304
355
|
});
|
|
305
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
|
+
|
|
306
382
|
it("unfollow_trader — deletes trader and returns JSON text (null body)", async () => {
|
|
307
383
|
fetchMock.mockResolvedValue({
|
|
308
384
|
ok: true,
|
|
@@ -322,7 +398,7 @@ describe("Tool Registration", () => {
|
|
|
322
398
|
expect(result.content[0].type).toBe("text");
|
|
323
399
|
expect(JSON.parse(result.content[0].text)).toEqual({ success: true });
|
|
324
400
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
325
|
-
expect(url).toContain("/api/v1/traders/0xabc");
|
|
401
|
+
expect(url).toContain("/api/v1/portfolio/traders/0xabc");
|
|
326
402
|
expect(init.method).toBe("DELETE");
|
|
327
403
|
});
|
|
328
404
|
|
|
@@ -337,7 +413,7 @@ describe("Tool Registration", () => {
|
|
|
337
413
|
|
|
338
414
|
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
339
415
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
340
|
-
expect(url).toContain("/api/v1/traders/0xabc/pause");
|
|
416
|
+
expect(url).toContain("/api/v1/portfolio/traders/0xabc/pause");
|
|
341
417
|
expect(init.method).toBe("POST");
|
|
342
418
|
});
|
|
343
419
|
|
|
@@ -352,7 +428,7 @@ describe("Tool Registration", () => {
|
|
|
352
428
|
|
|
353
429
|
expect(JSON.parse(result.content[0].text)).toEqual(responseData);
|
|
354
430
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
355
|
-
expect(url).toContain("/api/v1/traders/0xabc/resume");
|
|
431
|
+
expect(url).toContain("/api/v1/portfolio/traders/0xabc/resume");
|
|
356
432
|
expect(init.method).toBe("POST");
|
|
357
433
|
});
|
|
358
434
|
|
|
@@ -433,7 +509,7 @@ describe("Tool Registration", () => {
|
|
|
433
509
|
|
|
434
510
|
const tool = getTools(server)["get_portfolio"];
|
|
435
511
|
await expect(tool.handler({}, {})).rejects.toThrow(
|
|
436
|
-
/Carbon Copy API error 403
|
|
512
|
+
/Carbon Copy API error 403/,
|
|
437
513
|
);
|
|
438
514
|
});
|
|
439
515
|
});
|