@carbon-copy/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.github/workflows/publish.yml +23 -0
  2. package/CLAUDE.md +69 -0
  3. package/LICENSE +21 -0
  4. package/README.md +108 -0
  5. package/dist/__tests__/client.test.d.ts +2 -0
  6. package/dist/__tests__/client.test.d.ts.map +1 -0
  7. package/dist/__tests__/client.test.js +292 -0
  8. package/dist/__tests__/client.test.js.map +1 -0
  9. package/dist/__tests__/resources.test.d.ts +2 -0
  10. package/dist/__tests__/resources.test.d.ts.map +1 -0
  11. package/dist/__tests__/resources.test.js +176 -0
  12. package/dist/__tests__/resources.test.js.map +1 -0
  13. package/dist/__tests__/tools.test.d.ts +2 -0
  14. package/dist/__tests__/tools.test.d.ts.map +1 -0
  15. package/dist/__tests__/tools.test.js +317 -0
  16. package/dist/__tests__/tools.test.js.map +1 -0
  17. package/dist/client.d.ts +46 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +94 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +33 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/resources/portfolio.d.ts +4 -0
  26. package/dist/resources/portfolio.d.ts.map +1 -0
  27. package/dist/resources/portfolio.js +19 -0
  28. package/dist/resources/portfolio.js.map +1 -0
  29. package/dist/resources/traders.d.ts +4 -0
  30. package/dist/resources/traders.d.ts.map +1 -0
  31. package/dist/resources/traders.js +19 -0
  32. package/dist/resources/traders.js.map +1 -0
  33. package/dist/tools/account.d.ts +4 -0
  34. package/dist/tools/account.d.ts.map +1 -0
  35. package/dist/tools/account.js +32 -0
  36. package/dist/tools/account.js.map +1 -0
  37. package/dist/tools/orders.d.ts +4 -0
  38. package/dist/tools/orders.d.ts.map +1 -0
  39. package/dist/tools/orders.js +57 -0
  40. package/dist/tools/orders.js.map +1 -0
  41. package/dist/tools/portfolio.d.ts +4 -0
  42. package/dist/tools/portfolio.d.ts.map +1 -0
  43. package/dist/tools/portfolio.js +84 -0
  44. package/dist/tools/portfolio.js.map +1 -0
  45. package/dist/tools/traders.d.ts +4 -0
  46. package/dist/tools/traders.d.ts.map +1 -0
  47. package/dist/tools/traders.js +163 -0
  48. package/dist/tools/traders.js.map +1 -0
  49. package/package.json +43 -0
  50. package/src/__tests__/client.test.ts +392 -0
  51. package/src/__tests__/resources.test.ts +263 -0
  52. package/src/__tests__/tools.test.ts +440 -0
  53. package/src/client.ts +161 -0
  54. package/src/index.ts +41 -0
  55. package/src/resources/portfolio.ts +30 -0
  56. package/src/resources/traders.ts +29 -0
  57. package/src/tools/account.ts +48 -0
  58. package/src/tools/orders.ts +74 -0
  59. package/src/tools/portfolio.ts +106 -0
  60. package/src/tools/traders.ts +207 -0
  61. package/tsconfig.json +17 -0
  62. package/vitest.config.ts +9 -0
@@ -0,0 +1,440 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { CarbonCopyClient } from "../client.js";
4
+ import { registerPortfolioTools } from "../tools/portfolio.js";
5
+ import { registerTraderTools } from "../tools/traders.js";
6
+ import { registerOrderTools } from "../tools/orders.js";
7
+ import { registerAccountTools } from "../tools/account.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
+ "follow_trader",
53
+ "get_trader",
54
+ "update_trader",
55
+ "unfollow_trader",
56
+ "pause_trader",
57
+ "resume_trader",
58
+ "list_orders",
59
+ "get_order",
60
+ "get_account",
61
+ "health",
62
+ ] as const;
63
+
64
+ describe("Tool Registration", () => {
65
+ let server: McpServer;
66
+ let client: CarbonCopyClient;
67
+ let fetchMock: ReturnType<typeof vi.fn>;
68
+
69
+ beforeEach(() => {
70
+ fetchMock = vi.fn();
71
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
72
+
73
+ server = new McpServer({ name: "test", version: "0.0.1" });
74
+ client = new CarbonCopyClient("cc_test");
75
+
76
+ registerPortfolioTools(server, client);
77
+ registerTraderTools(server, client);
78
+ registerOrderTools(server, client);
79
+ registerAccountTools(server, client);
80
+ });
81
+
82
+ afterEach(() => {
83
+ vi.restoreAllMocks();
84
+ });
85
+
86
+ // -------------------------------------------------------------------------
87
+ // Count & names
88
+ // -------------------------------------------------------------------------
89
+
90
+ it("registers exactly 14 tools", () => {
91
+ const tools = getTools(server);
92
+ expect(Object.keys(tools)).toHaveLength(14);
93
+ });
94
+
95
+ it("registers all expected tool names", () => {
96
+ const tools = getTools(server);
97
+ for (const name of ALL_TOOL_NAMES) {
98
+ expect(tools).toHaveProperty(name);
99
+ }
100
+ });
101
+
102
+ it("all registered tools are enabled by default", () => {
103
+ const tools = getTools(server);
104
+ for (const tool of Object.values(tools)) {
105
+ expect(tool.enabled).toBe(true);
106
+ }
107
+ });
108
+
109
+ // -------------------------------------------------------------------------
110
+ // Annotations
111
+ // -------------------------------------------------------------------------
112
+
113
+ describe("tool annotations", () => {
114
+ it("get_portfolio — readOnlyHint:true, no destructiveHint", () => {
115
+ const { annotations } = getTools(server)["get_portfolio"];
116
+ expect(annotations?.readOnlyHint).toBe(true);
117
+ expect(annotations?.destructiveHint).toBeUndefined();
118
+ expect(annotations?.idempotentHint).toBeUndefined();
119
+ });
120
+
121
+ it("get_portfolio_history — readOnlyHint:true", () => {
122
+ const { annotations } = getTools(server)["get_portfolio_history"];
123
+ expect(annotations?.readOnlyHint).toBe(true);
124
+ });
125
+
126
+ it("get_positions — readOnlyHint:true", () => {
127
+ const { annotations } = getTools(server)["get_positions"];
128
+ expect(annotations?.readOnlyHint).toBe(true);
129
+ });
130
+
131
+ it("list_traders — readOnlyHint:true", () => {
132
+ const { annotations } = getTools(server)["list_traders"];
133
+ expect(annotations?.readOnlyHint).toBe(true);
134
+ });
135
+
136
+ it("follow_trader — readOnlyHint:false, no destructiveHint", () => {
137
+ const { annotations } = getTools(server)["follow_trader"];
138
+ expect(annotations?.readOnlyHint).toBe(false);
139
+ expect(annotations?.destructiveHint).toBeUndefined();
140
+ });
141
+
142
+ it("get_trader — readOnlyHint:true", () => {
143
+ const { annotations } = getTools(server)["get_trader"];
144
+ expect(annotations?.readOnlyHint).toBe(true);
145
+ });
146
+
147
+ it("update_trader — readOnlyHint:false, idempotentHint:true", () => {
148
+ const { annotations } = getTools(server)["update_trader"];
149
+ expect(annotations?.readOnlyHint).toBe(false);
150
+ expect(annotations?.idempotentHint).toBe(true);
151
+ });
152
+
153
+ it("unfollow_trader — readOnlyHint:false, destructiveHint:true", () => {
154
+ const { annotations } = getTools(server)["unfollow_trader"];
155
+ expect(annotations?.readOnlyHint).toBe(false);
156
+ expect(annotations?.destructiveHint).toBe(true);
157
+ });
158
+
159
+ it("pause_trader — readOnlyHint:false, idempotentHint:true", () => {
160
+ const { annotations } = getTools(server)["pause_trader"];
161
+ expect(annotations?.readOnlyHint).toBe(false);
162
+ expect(annotations?.idempotentHint).toBe(true);
163
+ });
164
+
165
+ it("resume_trader — readOnlyHint:false, idempotentHint:true", () => {
166
+ const { annotations } = getTools(server)["resume_trader"];
167
+ expect(annotations?.readOnlyHint).toBe(false);
168
+ expect(annotations?.idempotentHint).toBe(true);
169
+ });
170
+
171
+ it("list_orders — readOnlyHint:true", () => {
172
+ const { annotations } = getTools(server)["list_orders"];
173
+ expect(annotations?.readOnlyHint).toBe(true);
174
+ });
175
+
176
+ it("get_order — readOnlyHint:true", () => {
177
+ const { annotations } = getTools(server)["get_order"];
178
+ expect(annotations?.readOnlyHint).toBe(true);
179
+ });
180
+
181
+ it("get_account — readOnlyHint:true", () => {
182
+ const { annotations } = getTools(server)["get_account"];
183
+ expect(annotations?.readOnlyHint).toBe(true);
184
+ });
185
+
186
+ it("health — readOnlyHint:true", () => {
187
+ const { annotations } = getTools(server)["health"];
188
+ expect(annotations?.readOnlyHint).toBe(true);
189
+ });
190
+ });
191
+
192
+ // -------------------------------------------------------------------------
193
+ // Tool execution — verify handlers call client and return content
194
+ // -------------------------------------------------------------------------
195
+
196
+ describe("tool execution", () => {
197
+ it("get_portfolio — calls getPortfolio() and returns JSON text", async () => {
198
+ const responseData = { totalValue: 1000, pnl: 50 };
199
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
200
+
201
+ const tool = getTools(server)["get_portfolio"];
202
+ const result = (await tool.handler({}, {})) as {
203
+ content: { type: string; text: string }[];
204
+ };
205
+
206
+ expect(result.content).toHaveLength(1);
207
+ expect(result.content[0].type).toBe("text");
208
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
209
+ });
210
+
211
+ it("get_portfolio_history — passes params and returns JSON text", async () => {
212
+ const responseData = { items: [], cursor: null };
213
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
214
+
215
+ const tool = getTools(server)["get_portfolio_history"];
216
+ const result = (await tool.handler({ limit: 10, cursor: "abc" }, {})) as {
217
+ content: { type: string; text: string }[];
218
+ };
219
+
220
+ expect(result.content[0].type).toBe("text");
221
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
222
+
223
+ // Verify the correct URL was fetched
224
+ const [url] = fetchMock.mock.calls[0] as [string];
225
+ expect(url).toContain("/api/v1/portfolio/history");
226
+ expect(url).toContain("limit=10");
227
+ expect(url).toContain("cursor=abc");
228
+ });
229
+
230
+ it("get_positions — calls getPositions() and returns JSON text", async () => {
231
+ const responseData = { positions: [] };
232
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
233
+
234
+ const tool = getTools(server)["get_positions"];
235
+ const result = (await tool.handler({}, {})) as {
236
+ content: { type: string; text: string }[];
237
+ };
238
+
239
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
240
+ const [url] = fetchMock.mock.calls[0] as [string];
241
+ expect(url).toContain("/api/v1/portfolio/positions");
242
+ });
243
+
244
+ it("list_traders — calls getTraders() and returns JSON text", async () => {
245
+ const responseData = [{ wallet: "0xabc", copyPercentage: 50 }];
246
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
247
+
248
+ const tool = getTools(server)["list_traders"];
249
+ const result = (await tool.handler({}, {})) as {
250
+ content: { type: string; text: string }[];
251
+ };
252
+
253
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
254
+ const [url] = fetchMock.mock.calls[0] as [string];
255
+ expect(url).toContain("/api/v1/traders");
256
+ });
257
+
258
+ it("follow_trader — posts follow request and returns JSON text", async () => {
259
+ const responseData = { id: "follow-1", wallet: "0xabc" };
260
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
261
+
262
+ const tool = getTools(server)["follow_trader"];
263
+ const args = { walletAddress: "0xabc", copyPercentage: 50 };
264
+ const result = (await tool.handler(args, {})) as {
265
+ content: { type: string; text: string }[];
266
+ };
267
+
268
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
269
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
270
+ expect(url).toContain("/api/v1/traders");
271
+ expect(init.method).toBe("POST");
272
+ expect(JSON.parse(init.body as string)).toMatchObject(args);
273
+ });
274
+
275
+ it("get_trader — calls getTrader(wallet) and returns JSON text", async () => {
276
+ const responseData = { wallet: "0xabc", copyPercentage: 50 };
277
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
278
+
279
+ const tool = getTools(server)["get_trader"];
280
+ const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
281
+ content: { type: string; text: string }[];
282
+ };
283
+
284
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
285
+ const [url] = fetchMock.mock.calls[0] as [string];
286
+ expect(url).toContain("/api/v1/traders/0xabc");
287
+ });
288
+
289
+ it("update_trader — patches trader settings and returns JSON text", async () => {
290
+ const responseData = { wallet: "0xabc", copyPercentage: 75 };
291
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
292
+
293
+ const tool = getTools(server)["update_trader"];
294
+ const result = (await tool.handler(
295
+ { wallet: "0xabc", copyPercentage: 75 },
296
+ {}
297
+ )) as { content: { type: string; text: string }[] };
298
+
299
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
300
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
301
+ expect(url).toContain("/api/v1/traders/0xabc");
302
+ expect(init.method).toBe("PATCH");
303
+ expect(JSON.parse(init.body as string)).toEqual({ copyPercentage: 75 });
304
+ });
305
+
306
+ it("unfollow_trader — deletes trader and returns JSON text (null body)", async () => {
307
+ fetchMock.mockResolvedValue({
308
+ ok: true,
309
+ status: 204,
310
+ statusText: "No Content",
311
+ headers: { get: () => null },
312
+ json: vi.fn(),
313
+ text: vi.fn(),
314
+ clone: vi.fn(),
315
+ });
316
+
317
+ const tool = getTools(server)["unfollow_trader"];
318
+ const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
319
+ content: { type: string; text: string }[];
320
+ };
321
+
322
+ expect(result.content[0].type).toBe("text");
323
+ expect(JSON.parse(result.content[0].text)).toEqual({ success: true });
324
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
325
+ expect(url).toContain("/api/v1/traders/0xabc");
326
+ expect(init.method).toBe("DELETE");
327
+ });
328
+
329
+ it("pause_trader — pauses trader and returns JSON text", async () => {
330
+ const responseData = { paused: true };
331
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
332
+
333
+ const tool = getTools(server)["pause_trader"];
334
+ const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
335
+ content: { type: string; text: string }[];
336
+ };
337
+
338
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
339
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
340
+ expect(url).toContain("/api/v1/traders/0xabc/pause");
341
+ expect(init.method).toBe("POST");
342
+ });
343
+
344
+ it("resume_trader — resumes trader and returns JSON text", async () => {
345
+ const responseData = { paused: false };
346
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
347
+
348
+ const tool = getTools(server)["resume_trader"];
349
+ const result = (await tool.handler({ wallet: "0xabc" }, {})) as {
350
+ content: { type: string; text: string }[];
351
+ };
352
+
353
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
354
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
355
+ expect(url).toContain("/api/v1/traders/0xabc/resume");
356
+ expect(init.method).toBe("POST");
357
+ });
358
+
359
+ it("list_orders — calls getOrders() with params and returns JSON text", async () => {
360
+ const responseData = { orders: [], cursor: null };
361
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
362
+
363
+ const tool = getTools(server)["list_orders"];
364
+ const result = (await tool.handler({ status: "filled" }, {})) as {
365
+ content: { type: string; text: string }[];
366
+ };
367
+
368
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
369
+ const [url] = fetchMock.mock.calls[0] as [string];
370
+ expect(url).toContain("/api/v1/orders");
371
+ expect(url).toContain("status=filled");
372
+ });
373
+
374
+ it("get_order — calls getOrder(id) and returns JSON text", async () => {
375
+ const responseData = { id: "id123", status: "filled" };
376
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
377
+
378
+ const tool = getTools(server)["get_order"];
379
+ const result = (await tool.handler({ id: "id123" }, {})) as {
380
+ content: { type: string; text: string }[];
381
+ };
382
+
383
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
384
+ const [url] = fetchMock.mock.calls[0] as [string];
385
+ expect(url).toContain("/api/v1/orders/id123");
386
+ });
387
+
388
+ it("get_account — calls getAccount() and returns JSON text", async () => {
389
+ const responseData = { wallet: "0xuser", balance: 500 };
390
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
391
+
392
+ const tool = getTools(server)["get_account"];
393
+ const result = (await tool.handler({}, {})) as {
394
+ content: { type: string; text: string }[];
395
+ };
396
+
397
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
398
+ const [url] = fetchMock.mock.calls[0] as [string];
399
+ expect(url).toContain("/api/v1/account");
400
+ });
401
+
402
+ it("health — calls health() and returns JSON text", async () => {
403
+ const responseData = { status: "ok" };
404
+ fetchMock.mockResolvedValue(createMockFetchResponse(responseData));
405
+
406
+ const tool = getTools(server)["health"];
407
+ const result = (await tool.handler({}, {})) as {
408
+ content: { type: string; text: string }[];
409
+ };
410
+
411
+ expect(JSON.parse(result.content[0].text)).toEqual(responseData);
412
+ const [url] = fetchMock.mock.calls[0] as [string];
413
+ expect(url).toContain("/api/v1/health");
414
+ });
415
+ });
416
+
417
+ // -------------------------------------------------------------------------
418
+ // Error propagation through tools
419
+ // -------------------------------------------------------------------------
420
+
421
+ describe("tool error propagation", () => {
422
+ it("get_portfolio rejects when client throws an API error", async () => {
423
+ fetchMock.mockResolvedValue({
424
+ ok: false,
425
+ status: 403,
426
+ statusText: "Forbidden",
427
+ headers: { get: () => null },
428
+ json: vi.fn().mockResolvedValue({ message: "forbidden" }),
429
+ clone: vi.fn().mockReturnValue({
430
+ json: vi.fn().mockResolvedValue({ message: "forbidden" }),
431
+ }),
432
+ });
433
+
434
+ const tool = getTools(server)["get_portfolio"];
435
+ await expect(tool.handler({}, {})).rejects.toThrow(
436
+ /Carbon Copy API error 403/
437
+ );
438
+ });
439
+ });
440
+ });
package/src/client.ts ADDED
@@ -0,0 +1,161 @@
1
+ const BASE_URL = "https://carboncopy.news";
2
+
3
+ function buildQuery(params: Record<string, string | number | undefined>): string {
4
+ const qs = Object.entries(params)
5
+ .filter(([, v]) => v !== undefined)
6
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
7
+ .join("&");
8
+ return qs ? `?${qs}` : "";
9
+ }
10
+
11
+ export class CarbonCopyClient {
12
+ private readonly apiKey: string;
13
+
14
+ constructor(apiKey: string) {
15
+ this.apiKey = apiKey;
16
+ }
17
+
18
+ private async request<T>(
19
+ method: string,
20
+ path: string,
21
+ body?: unknown
22
+ ): Promise<T> {
23
+ const url = `${BASE_URL}${path}`;
24
+ const headers: Record<string, string> = {
25
+ Authorization: `Bearer ${this.apiKey}`,
26
+ "Content-Type": "application/json",
27
+ };
28
+
29
+ const res = await fetch(url, {
30
+ method,
31
+ headers,
32
+ body: body !== undefined ? JSON.stringify(body) : undefined,
33
+ });
34
+
35
+ if (!res.ok) {
36
+ const cloned = res.clone();
37
+ let errorBody: unknown;
38
+ try {
39
+ errorBody = await cloned.json();
40
+ } catch {
41
+ errorBody = await res.text();
42
+ }
43
+ throw new Error(
44
+ `Carbon Copy API error ${res.status} ${res.statusText}: ${JSON.stringify(errorBody)}`
45
+ );
46
+ }
47
+
48
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
49
+ return undefined as unknown as T;
50
+ }
51
+
52
+ const text = await res.text();
53
+ if (!text) return undefined as unknown as T;
54
+ return JSON.parse(text) as T;
55
+ }
56
+
57
+ // Portfolio
58
+ getPortfolio(): Promise<unknown> {
59
+ return this.request("GET", "/api/v1/portfolio");
60
+ }
61
+
62
+ getPortfolioHistory(params?: {
63
+ limit?: number;
64
+ cursor?: string;
65
+ since?: string;
66
+ until?: string;
67
+ }): Promise<unknown> {
68
+ const qs = buildQuery(params ?? {});
69
+ return this.request("GET", `/api/v1/portfolio/history${qs}`);
70
+ }
71
+
72
+ getPositions(params?: {
73
+ limit?: number;
74
+ cursor?: string;
75
+ since?: string;
76
+ until?: string;
77
+ }): Promise<unknown> {
78
+ const qs = buildQuery(params ?? {});
79
+ return this.request("GET", `/api/v1/portfolio/positions${qs}`);
80
+ }
81
+
82
+ // Traders
83
+ getTraders(): Promise<unknown> {
84
+ return this.request("GET", "/api/v1/traders");
85
+ }
86
+
87
+ followTrader(body: {
88
+ walletAddress: string;
89
+ copyPercentage: number;
90
+ maxCopyAmount?: number;
91
+ notificationsEnabled?: boolean;
92
+ }): Promise<unknown> {
93
+ return this.request("POST", "/api/v1/traders", body);
94
+ }
95
+
96
+ getTrader(wallet: string): Promise<unknown> {
97
+ return this.request("GET", `/api/v1/traders/${encodeURIComponent(wallet)}`);
98
+ }
99
+
100
+ updateTrader(
101
+ wallet: string,
102
+ body: {
103
+ copyPercentage?: number;
104
+ maxCopyAmount?: number;
105
+ notificationsEnabled?: boolean;
106
+ copyTradingEnabled?: boolean;
107
+ }
108
+ ): Promise<unknown> {
109
+ return this.request(
110
+ "PATCH",
111
+ `/api/v1/traders/${encodeURIComponent(wallet)}`,
112
+ body
113
+ );
114
+ }
115
+
116
+ unfollowTrader(wallet: string): Promise<unknown> {
117
+ return this.request(
118
+ "DELETE",
119
+ `/api/v1/traders/${encodeURIComponent(wallet)}`
120
+ );
121
+ }
122
+
123
+ pauseTrader(wallet: string): Promise<unknown> {
124
+ return this.request(
125
+ "POST",
126
+ `/api/v1/traders/${encodeURIComponent(wallet)}/pause`
127
+ );
128
+ }
129
+
130
+ resumeTrader(wallet: string): Promise<unknown> {
131
+ return this.request(
132
+ "POST",
133
+ `/api/v1/traders/${encodeURIComponent(wallet)}/resume`
134
+ );
135
+ }
136
+
137
+ // Orders
138
+ getOrders(params?: {
139
+ status?: string;
140
+ limit?: number;
141
+ cursor?: string;
142
+ since?: string;
143
+ until?: string;
144
+ }): Promise<unknown> {
145
+ const qs = buildQuery(params ?? {});
146
+ return this.request("GET", `/api/v1/orders${qs}`);
147
+ }
148
+
149
+ getOrder(id: string): Promise<unknown> {
150
+ return this.request("GET", `/api/v1/orders/${encodeURIComponent(id)}`);
151
+ }
152
+
153
+ // Account
154
+ getAccount(): Promise<unknown> {
155
+ return this.request("GET", "/api/v1/account");
156
+ }
157
+
158
+ health(): Promise<unknown> {
159
+ return this.request("GET", "/api/v1/health");
160
+ }
161
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
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
+ import { CarbonCopyClient } from "./client.js";
6
+ import { registerPortfolioTools } from "./tools/portfolio.js";
7
+ import { registerTraderTools } from "./tools/traders.js";
8
+ import { registerOrderTools } from "./tools/orders.js";
9
+ import { registerAccountTools } from "./tools/account.js";
10
+ import { registerPortfolioResources } from "./resources/portfolio.js";
11
+ import { registerTraderResources } from "./resources/traders.js";
12
+
13
+ const apiKey = process.env.CARBONCOPY_API_KEY;
14
+ if (!apiKey) {
15
+ console.error(
16
+ "Error: CARBONCOPY_API_KEY environment variable is required.\n" +
17
+ "Set it to your Carbon Copy API key (format: cc_<64 hex chars>)."
18
+ );
19
+ process.exit(1);
20
+ }
21
+
22
+ const client = new CarbonCopyClient(apiKey);
23
+
24
+ const server = new McpServer({
25
+ name: "carboncopy",
26
+ version: "0.1.0",
27
+ });
28
+
29
+ // Register tools
30
+ registerPortfolioTools(server, client);
31
+ registerTraderTools(server, client);
32
+ registerOrderTools(server, client);
33
+ registerAccountTools(server, client);
34
+
35
+ // Register resources
36
+ registerPortfolioResources(server, client);
37
+ registerTraderResources(server, client);
38
+
39
+ // Connect via stdio transport
40
+ const transport = new StdioServerTransport();
41
+ await server.connect(transport);
@@ -0,0 +1,30 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CarbonCopyClient } from "../client.js";
3
+
4
+ export function registerPortfolioResources(
5
+ server: McpServer,
6
+ client: CarbonCopyClient
7
+ ): void {
8
+ server.registerResource(
9
+ "portfolio",
10
+ "carboncopy://portfolio",
11
+ {
12
+ title: "Carbon Copy Portfolio",
13
+ description:
14
+ "Current portfolio snapshot including total value, P&L, and allocation.",
15
+ mimeType: "application/json",
16
+ },
17
+ async () => {
18
+ const data = await client.getPortfolio();
19
+ return {
20
+ contents: [
21
+ {
22
+ uri: "carboncopy://portfolio",
23
+ mimeType: "application/json",
24
+ text: JSON.stringify(data ?? {}, null, 2),
25
+ },
26
+ ],
27
+ };
28
+ }
29
+ );
30
+ }
@@ -0,0 +1,29 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CarbonCopyClient } from "../client.js";
3
+
4
+ export function registerTraderResources(
5
+ server: McpServer,
6
+ client: CarbonCopyClient
7
+ ): void {
8
+ server.registerResource(
9
+ "traders",
10
+ "carboncopy://traders",
11
+ {
12
+ title: "Carbon Copy Traders",
13
+ description: "List of all traders you are currently following.",
14
+ mimeType: "application/json",
15
+ },
16
+ async () => {
17
+ const data = await client.getTraders();
18
+ return {
19
+ contents: [
20
+ {
21
+ uri: "carboncopy://traders",
22
+ mimeType: "application/json",
23
+ text: JSON.stringify(data ?? [], null, 2),
24
+ },
25
+ ],
26
+ };
27
+ }
28
+ );
29
+ }