@carbon-copy/mcp 0.3.0 → 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.
@@ -1,263 +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 { registerPortfolioResources } from "../resources/portfolio.js";
5
- import { registerTraderResources } from "../resources/traders.js";
6
-
7
- // ---------------------------------------------------------------------------
8
- // Helpers
9
- // ---------------------------------------------------------------------------
10
-
11
- type RegisteredResource = {
12
- name: string;
13
- title?: string;
14
- metadata?: {
15
- title?: string;
16
- description?: string;
17
- mimeType?: string;
18
- };
19
- readCallback: (uri: URL, extra: unknown) => Promise<unknown>;
20
- enabled: boolean;
21
- };
22
-
23
- type ServerInternals = {
24
- _registeredResources: Record<string, RegisteredResource>;
25
- };
26
-
27
- function getResources(server: McpServer) {
28
- return (server as unknown as ServerInternals)._registeredResources;
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
- // Tests
45
- // ---------------------------------------------------------------------------
46
-
47
- describe("Resource Registration", () => {
48
- let server: McpServer;
49
- let client: CarbonCopyClient;
50
- let fetchMock: ReturnType<typeof vi.fn>;
51
-
52
- beforeEach(() => {
53
- fetchMock = vi.fn();
54
- globalThis.fetch = fetchMock as unknown as typeof fetch;
55
-
56
- server = new McpServer({ name: "test", version: "0.0.1" });
57
- client = new CarbonCopyClient("cc_test");
58
-
59
- registerPortfolioResources(server, client);
60
- registerTraderResources(server, client);
61
- });
62
-
63
- afterEach(() => {
64
- vi.restoreAllMocks();
65
- });
66
-
67
- // -------------------------------------------------------------------------
68
- // Registration checks
69
- // -------------------------------------------------------------------------
70
-
71
- it("registers exactly 2 resources", () => {
72
- const resources = getResources(server);
73
- expect(Object.keys(resources)).toHaveLength(2);
74
- });
75
-
76
- it("registers the portfolio resource at carboncopy://portfolio", () => {
77
- const resources = getResources(server);
78
- expect(resources).toHaveProperty("carboncopy://portfolio");
79
- });
80
-
81
- it("registers the traders resource at carboncopy://traders", () => {
82
- const resources = getResources(server);
83
- expect(resources).toHaveProperty("carboncopy://traders");
84
- });
85
-
86
- it("portfolio resource has name 'portfolio'", () => {
87
- const resources = getResources(server);
88
- expect(resources["carboncopy://portfolio"].name).toBe("portfolio");
89
- });
90
-
91
- it("traders resource has name 'traders'", () => {
92
- const resources = getResources(server);
93
- expect(resources["carboncopy://traders"].name).toBe("traders");
94
- });
95
-
96
- it("portfolio resource has mimeType application/json", () => {
97
- const resources = getResources(server);
98
- const resource = resources["carboncopy://portfolio"];
99
- expect(resource.metadata?.mimeType).toBe("application/json");
100
- });
101
-
102
- it("traders resource has mimeType application/json", () => {
103
- const resources = getResources(server);
104
- const resource = resources["carboncopy://traders"];
105
- expect(resource.metadata?.mimeType).toBe("application/json");
106
- });
107
-
108
- it("all resources are enabled by default", () => {
109
- const resources = getResources(server);
110
- for (const resource of Object.values(resources)) {
111
- expect(resource.enabled).toBe(true);
112
- }
113
- });
114
-
115
- // -------------------------------------------------------------------------
116
- // Portfolio resource — readCallback
117
- // -------------------------------------------------------------------------
118
-
119
- describe("portfolio resource — read", () => {
120
- it("returns a content block with URI carboncopy://portfolio", async () => {
121
- const portfolioData = { totalValue: 1234.56, pnl: 123.45 };
122
- fetchMock.mockResolvedValue(createMockFetchResponse(portfolioData));
123
-
124
- const resource = getResources(server)["carboncopy://portfolio"];
125
- const uri = new URL("carboncopy://portfolio");
126
- const result = (await resource.readCallback(uri, {})) as {
127
- contents: { uri: string; mimeType: string; text: string }[];
128
- };
129
-
130
- expect(result.contents).toHaveLength(1);
131
- expect(result.contents[0].uri).toBe("carboncopy://portfolio");
132
- });
133
-
134
- it("returns application/json mimeType in content", async () => {
135
- fetchMock.mockResolvedValue(createMockFetchResponse({ v: 1 }));
136
-
137
- const resource = getResources(server)["carboncopy://portfolio"];
138
- const result = (await resource.readCallback(
139
- new URL("carboncopy://portfolio"),
140
- {},
141
- )) as { contents: { uri: string; mimeType: string; text: string }[] };
142
-
143
- expect(result.contents[0].mimeType).toBe("application/json");
144
- });
145
-
146
- it("returns JSON-stringified portfolio data as text", async () => {
147
- const portfolioData = { totalValue: 500, positions: [] };
148
- fetchMock.mockResolvedValue(createMockFetchResponse(portfolioData));
149
-
150
- const resource = getResources(server)["carboncopy://portfolio"];
151
- const result = (await resource.readCallback(
152
- new URL("carboncopy://portfolio"),
153
- {},
154
- )) as { contents: { uri: string; mimeType: string; text: string }[] };
155
-
156
- expect(JSON.parse(result.contents[0].text)).toEqual(portfolioData);
157
- });
158
-
159
- it("calls GET /api/v1/portfolio when portfolio resource is read", async () => {
160
- fetchMock.mockResolvedValue(createMockFetchResponse({}));
161
-
162
- const resource = getResources(server)["carboncopy://portfolio"];
163
- await resource.readCallback(new URL("carboncopy://portfolio"), {});
164
-
165
- const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
166
- expect(url).toContain("/api/v1/portfolio");
167
- expect(init.method).toBe("GET");
168
- });
169
-
170
- it("propagates API errors from the portfolio resource read", async () => {
171
- fetchMock.mockResolvedValue({
172
- ok: false,
173
- status: 401,
174
- statusText: "Unauthorized",
175
- headers: { get: () => null },
176
- clone: vi.fn().mockReturnValue({
177
- json: vi.fn().mockResolvedValue({ message: "unauthorized" }),
178
- }),
179
- });
180
-
181
- const resource = getResources(server)["carboncopy://portfolio"];
182
- await expect(
183
- resource.readCallback(new URL("carboncopy://portfolio"), {}),
184
- ).rejects.toThrow(/Carbon Copy API error 401/);
185
- });
186
- });
187
-
188
- // -------------------------------------------------------------------------
189
- // Traders resource — readCallback
190
- // -------------------------------------------------------------------------
191
-
192
- describe("traders resource — read", () => {
193
- it("returns a content block with URI carboncopy://traders", async () => {
194
- const tradersData = [{ wallet: "0xabc", copyPercentage: 50 }];
195
- fetchMock.mockResolvedValue(createMockFetchResponse(tradersData));
196
-
197
- const resource = getResources(server)["carboncopy://traders"];
198
- const result = (await resource.readCallback(
199
- new URL("carboncopy://traders"),
200
- {},
201
- )) as { contents: { uri: string; mimeType: string; text: string }[] };
202
-
203
- expect(result.contents).toHaveLength(1);
204
- expect(result.contents[0].uri).toBe("carboncopy://traders");
205
- });
206
-
207
- it("returns application/json mimeType in content", async () => {
208
- fetchMock.mockResolvedValue(createMockFetchResponse([]));
209
-
210
- const resource = getResources(server)["carboncopy://traders"];
211
- const result = (await resource.readCallback(
212
- new URL("carboncopy://traders"),
213
- {},
214
- )) as { contents: { uri: string; mimeType: string; text: string }[] };
215
-
216
- expect(result.contents[0].mimeType).toBe("application/json");
217
- });
218
-
219
- it("returns JSON-stringified traders list as text", async () => {
220
- const tradersData = [
221
- { wallet: "0xabc", copyPercentage: 50 },
222
- { wallet: "0xdef", copyPercentage: 25 },
223
- ];
224
- fetchMock.mockResolvedValue(createMockFetchResponse(tradersData));
225
-
226
- const resource = getResources(server)["carboncopy://traders"];
227
- const result = (await resource.readCallback(
228
- new URL("carboncopy://traders"),
229
- {},
230
- )) as { contents: { uri: string; mimeType: string; text: string }[] };
231
-
232
- expect(JSON.parse(result.contents[0].text)).toEqual(tradersData);
233
- });
234
-
235
- it("calls GET /api/v1/portfolio/traders when traders resource is read", async () => {
236
- fetchMock.mockResolvedValue(createMockFetchResponse([]));
237
-
238
- const resource = getResources(server)["carboncopy://traders"];
239
- await resource.readCallback(new URL("carboncopy://traders"), {});
240
-
241
- const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
242
- expect(url).toContain("/api/v1/portfolio/traders");
243
- expect(init.method).toBe("GET");
244
- });
245
-
246
- it("propagates API errors from the traders resource read", async () => {
247
- fetchMock.mockResolvedValue({
248
- ok: false,
249
- status: 403,
250
- statusText: "Forbidden",
251
- headers: { get: () => null },
252
- clone: vi.fn().mockReturnValue({
253
- json: vi.fn().mockResolvedValue({ message: "forbidden" }),
254
- }),
255
- });
256
-
257
- const resource = getResources(server)["carboncopy://traders"];
258
- await expect(
259
- resource.readCallback(new URL("carboncopy://traders"), {}),
260
- ).rejects.toThrow(/Carbon Copy API error 403/);
261
- });
262
- });
263
- });