@codespar/mcp-circle 0.1.0 → 0.1.2
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/README.md +4 -0
- package/dist/index.js +42 -5
- package/package.json +3 -2
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +50 -0
- package/src/index.ts +27 -5
package/README.md
CHANGED
|
@@ -107,6 +107,10 @@ Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [
|
|
|
107
107
|
- [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
|
|
108
108
|
- [Landing Page](https://codespar.dev/mcp)
|
|
109
109
|
|
|
110
|
+
## Enterprise
|
|
111
|
+
|
|
112
|
+
Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
|
|
113
|
+
|
|
110
114
|
## License
|
|
111
115
|
|
|
112
116
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
22
24
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
23
25
|
const API_KEY = process.env.CIRCLE_API_KEY || "";
|
|
24
26
|
const BASE_URL = "https://api.circle.com/v1";
|
|
@@ -209,11 +211,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
209
211
|
}
|
|
210
212
|
});
|
|
211
213
|
async function main() {
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
215
|
+
const { default: express } = await import("express");
|
|
216
|
+
const { randomUUID } = await import("node:crypto");
|
|
217
|
+
const app = express();
|
|
218
|
+
app.use(express.json());
|
|
219
|
+
const transports = new Map();
|
|
220
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
221
|
+
app.post("/mcp", async (req, res) => {
|
|
222
|
+
const sid = req.headers["mcp-session-id"];
|
|
223
|
+
if (sid && transports.has(sid)) {
|
|
224
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
228
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
229
|
+
t.onclose = () => { if (t.sessionId)
|
|
230
|
+
transports.delete(t.sessionId); };
|
|
231
|
+
const s = new Server({ name: "mcp-circle", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
232
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
233
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
234
|
+
await s.connect(t);
|
|
235
|
+
await t.handleRequest(req, res, req.body);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
239
|
+
});
|
|
240
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
241
|
+
await transports.get(sid).handleRequest(req, res);
|
|
242
|
+
else
|
|
243
|
+
res.status(400).send("Invalid session"); });
|
|
244
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
245
|
+
await transports.get(sid).handleRequest(req, res);
|
|
246
|
+
else
|
|
247
|
+
res.status(400).send("Invalid session"); });
|
|
248
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
249
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const transport = new StdioServerTransport();
|
|
253
|
+
await server.connect(transport);
|
|
215
254
|
}
|
|
216
|
-
const transport = new StdioServerTransport();
|
|
217
|
-
await server.connect(transport);
|
|
218
255
|
}
|
|
219
256
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespar/mcp-circle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "MCP server for Circle — USDC payments, wallets, payouts, transfers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"crypto",
|
|
27
27
|
"stablecoin",
|
|
28
28
|
"payments"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"mcpName": "io.github.codespar/mcp-circle"
|
|
30
31
|
}
|
package/server.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.codespar/mcp-circle",
|
|
4
|
+
"description": "MCP server for Circle — USDC payments, wallets, payouts, transfers",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/crypto/circle"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.1.2",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-circle",
|
|
15
|
+
"version": "0.1.2",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "CIRCLE_API_KEY",
|
|
22
|
+
"description": "API key for circle",
|
|
23
|
+
"isRequired": true,
|
|
24
|
+
"format": "string",
|
|
25
|
+
"isSecret": true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
let listToolsHandler: Function;
|
|
4
|
+
let callToolHandler: Function;
|
|
5
|
+
|
|
6
|
+
vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
|
|
7
|
+
class FakeServer {
|
|
8
|
+
constructor() {}
|
|
9
|
+
setRequestHandler(schema: any, handler: Function) {
|
|
10
|
+
if (JSON.stringify(schema).includes("tools/list")) listToolsHandler = handler;
|
|
11
|
+
if (JSON.stringify(schema).includes("tools/call")) callToolHandler = handler;
|
|
12
|
+
}
|
|
13
|
+
connect() { return Promise.resolve(); }
|
|
14
|
+
}
|
|
15
|
+
return { Server: FakeServer };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: class {} }));
|
|
19
|
+
|
|
20
|
+
process.env.CIRCLE_API_KEY = "test-key";
|
|
21
|
+
|
|
22
|
+
const mockFetch = vi.fn();
|
|
23
|
+
global.fetch = mockFetch as any;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
vi.resetModules();
|
|
27
|
+
listToolsHandler = undefined as any;
|
|
28
|
+
callToolHandler = undefined as any;
|
|
29
|
+
mockFetch.mockReset();
|
|
30
|
+
global.fetch = mockFetch as any;
|
|
31
|
+
await import("../index.js");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("mcp-circle", () => {
|
|
35
|
+
it("should register 10 tools", async () => {
|
|
36
|
+
const result = await listToolsHandler();
|
|
37
|
+
expect(result.tools).toHaveLength(10);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should call correct API endpoint for create_wallet", async () => {
|
|
41
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: { walletId: "w1" } }) });
|
|
42
|
+
|
|
43
|
+
await callToolHandler({ params: { name: "create_wallet", arguments: { idempotencyKey: "key1" } } });
|
|
44
|
+
|
|
45
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
46
|
+
expect(url).toContain("api.circle.com/v1/wallets");
|
|
47
|
+
expect(opts.method).toBe("POST");
|
|
48
|
+
expect(opts.headers.Authorization).toBe("Bearer test-key");
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
23
23
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
25
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
24
26
|
import {
|
|
25
27
|
CallToolRequestSchema,
|
|
26
28
|
ListToolsRequestSchema,
|
|
@@ -216,12 +218,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
216
218
|
});
|
|
217
219
|
|
|
218
220
|
async function main() {
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
222
|
+
const { default: express } = await import("express");
|
|
223
|
+
const { randomUUID } = await import("node:crypto");
|
|
224
|
+
const app = express();
|
|
225
|
+
app.use(express.json());
|
|
226
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
227
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
228
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
229
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
230
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
231
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
232
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
233
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
234
|
+
const s = new Server({ name: "mcp-circle", version: "0.1.0" }, { capabilities: { tools: {} } }); (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); await s.connect(t);
|
|
235
|
+
await t.handleRequest(req, res, req.body); return;
|
|
236
|
+
}
|
|
237
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
238
|
+
});
|
|
239
|
+
app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
240
|
+
app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
241
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
242
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
243
|
+
} else {
|
|
244
|
+
const transport = new StdioServerTransport();
|
|
245
|
+
await server.connect(transport);
|
|
222
246
|
}
|
|
223
|
-
const transport = new StdioServerTransport();
|
|
224
|
-
await server.connect(transport);
|
|
225
247
|
}
|
|
226
248
|
|
|
227
249
|
main().catch(console.error);
|