@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.
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
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.fn().mockResolvedValue(
35
- bodyText || (body !== null ? JSON.stringify(body) : "")
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.news";
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
- walletAddress: "0xabc",
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(JSON.stringify(payload));
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("followTrader({...}) → POST /api/v1/traders with body", async () => {
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
- walletAddress: "0xabc",
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(body);
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(createMockResponse({ status: 204, ok: true }));
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(`${BASE}/api/v1/traders/${encodeURIComponent(wallet)}`);
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(createMockResponse({ status: 204, ok: true }));
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(`${BASE}/api/v1/traders/${encodeURIComponent(wallet)}`);
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 14 tools", () => {
93
+ it("registers exactly 17 tools", () => {
91
94
  const tools = getTools(server);
92
- expect(Object.keys(tools)).toHaveLength(14);
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 = { walletAddress: "0xabc", copyPercentage: 50 };
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(args);
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
  });