bcap-x402-mcp 1.0.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 (2) hide show
  1. package/build/index.js +181 -0
  2. package/package.json +37 -0
package/build/index.js ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import axios, { AxiosError } from "axios";
5
+ import { x402Client, wrapAxiosWithPayment } from "@x402/axios";
6
+ import { ExactEvmScheme } from "@x402/evm/exact/client";
7
+ import { toClientEvmSigner } from "@x402/evm";
8
+ import { ExactSvmScheme } from "@x402/svm/exact/client";
9
+ import { privateKeyToAccount } from "viem/accounts";
10
+ import { createPublicClient, http } from "viem";
11
+ import { base, baseSepolia } from "viem/chains";
12
+ import { createKeyPairSignerFromBytes } from "@solana/signers";
13
+ import { base58 } from "@scure/base";
14
+ import { z } from "zod";
15
+ const evmPrivateKey = process.env.EVM_PRIVATE_KEY;
16
+ const svmPrivateKey = process.env.SVM_PRIVATE_KEY;
17
+ const baseURL = process.env.RESOURCE_SERVER_URL || "http://localhost:3001";
18
+ const network = process.env.NETWORK || "testnet"; // "mainnet" or "testnet"
19
+ if (!evmPrivateKey && !svmPrivateKey) {
20
+ console.error("At least one of EVM_PRIVATE_KEY or SVM_PRIVATE_KEY must be provided");
21
+ process.exit(1);
22
+ }
23
+ function getEvmChain() {
24
+ return network === "mainnet" ? base : baseSepolia;
25
+ }
26
+ async function createPaymentClient() {
27
+ const client = new x402Client();
28
+ if (evmPrivateKey) {
29
+ const account = privateKeyToAccount(evmPrivateKey);
30
+ const publicClient = createPublicClient({
31
+ chain: getEvmChain(),
32
+ transport: http(),
33
+ });
34
+ const evmSigner = toClientEvmSigner(account, publicClient);
35
+ client.register("eip155:*", new ExactEvmScheme(evmSigner));
36
+ }
37
+ if (svmPrivateKey) {
38
+ const svmSigner = await createKeyPairSignerFromBytes(base58.decode(svmPrivateKey));
39
+ client.register("solana:*", new ExactSvmScheme(svmSigner));
40
+ }
41
+ return wrapAxiosWithPayment(axios.create({ baseURL }), client);
42
+ }
43
+ /** Format an error into a text content response for the agent */
44
+ function errorResponse(err) {
45
+ let message;
46
+ if (err instanceof AxiosError) {
47
+ const status = err.response?.status;
48
+ const data = err.response?.data;
49
+ message = `HTTP ${status}: ${JSON.stringify(data ?? err.message)}`;
50
+ }
51
+ else if (err instanceof Error) {
52
+ message = err.message;
53
+ }
54
+ else {
55
+ message = String(err);
56
+ }
57
+ return {
58
+ content: [{ type: "text", text: message }],
59
+ isError: true,
60
+ };
61
+ }
62
+ async function main() {
63
+ const api = await createPaymentClient();
64
+ const server = new McpServer({
65
+ name: "bcap-x402",
66
+ version: "1.0.0",
67
+ });
68
+ // --- Tools ---
69
+ server.registerTool("get_metrics", {
70
+ title: "Get Metrics",
71
+ description: "Get service metrics: total tweets posted, current cost per tweet in USD, accepted payment networks, and estimated waiting time in seconds.",
72
+ inputSchema: {},
73
+ }, async () => {
74
+ try {
75
+ const res = await api.get("/metrics");
76
+ return {
77
+ content: [
78
+ { type: "text", text: JSON.stringify(res.data) },
79
+ ],
80
+ };
81
+ }
82
+ catch (err) {
83
+ return errorResponse(err);
84
+ }
85
+ });
86
+ server.registerTool("check_tweet_exists", {
87
+ title: "Check Tweet Exists",
88
+ description: "Check if a tweet with the given text has already been posted (duplicate detection). Returns { exists: boolean }.",
89
+ inputSchema: {
90
+ text: z.string().describe("The tweet text to check for duplicates"),
91
+ },
92
+ }, async ({ text }) => {
93
+ try {
94
+ const res = await api.post("/tweet-exists", { text });
95
+ return {
96
+ content: [
97
+ { type: "text", text: JSON.stringify(res.data) },
98
+ ],
99
+ };
100
+ }
101
+ catch (err) {
102
+ return errorResponse(err);
103
+ }
104
+ });
105
+ server.registerTool("post_tweet", {
106
+ title: "Post Tweet",
107
+ description: `Submit a tweet for posting. This endpoint is x402 payment-gated — payment is handled automatically using the configured wallet.
108
+
109
+ On success the tweet is queued for admin approval (status 202). Returns queue_id and status.`,
110
+ inputSchema: {
111
+ text: z
112
+ .string()
113
+ .describe("The tweet text content (max 280 characters)"),
114
+ image_id: z
115
+ .string()
116
+ .optional()
117
+ .describe("Optional image ID from a previous upload_image call"),
118
+ },
119
+ }, async ({ text, image_id }) => {
120
+ try {
121
+ const body = { text };
122
+ if (image_id) {
123
+ body.image_id = image_id;
124
+ }
125
+ const res = await api.post("/post-tweet", body);
126
+ return {
127
+ content: [
128
+ { type: "text", text: JSON.stringify(res.data) },
129
+ ],
130
+ };
131
+ }
132
+ catch (err) {
133
+ return errorResponse(err);
134
+ }
135
+ });
136
+ server.registerTool("upload_image", {
137
+ title: "Upload Image",
138
+ description: "Upload a base64-encoded image to attach to a future tweet. Returns an image_id to pass to post_tweet.",
139
+ inputSchema: {
140
+ image_base64: z.string().describe("Base64-encoded image data"),
141
+ filename: z
142
+ .string()
143
+ .optional()
144
+ .describe("Filename with extension (e.g. photo.png). Determines media type. Defaults to image.png."),
145
+ },
146
+ }, async ({ image_base64, filename }) => {
147
+ try {
148
+ const name = filename || "image.png";
149
+ const imageBuffer = Buffer.from(image_base64, "base64");
150
+ const ext = name.split(".").pop()?.toLowerCase() || "png";
151
+ const mimeMap = {
152
+ jpg: "image/jpeg",
153
+ jpeg: "image/jpeg",
154
+ png: "image/png",
155
+ gif: "image/gif",
156
+ webp: "image/webp",
157
+ mp4: "video/mp4",
158
+ };
159
+ const mime = mimeMap[ext] || "image/png";
160
+ const form = new FormData();
161
+ form.append("image", new Blob([imageBuffer], { type: mime }), name);
162
+ const res = await api.post("/upload-image", form);
163
+ return {
164
+ content: [
165
+ { type: "text", text: JSON.stringify(res.data) },
166
+ ],
167
+ };
168
+ }
169
+ catch (err) {
170
+ return errorResponse(err);
171
+ }
172
+ });
173
+ // --- Connect ---
174
+ const transport = new StdioServerTransport();
175
+ await server.connect(transport);
176
+ console.error("bcap-x402 MCP server running on stdio");
177
+ }
178
+ main().catch((error) => {
179
+ console.error("Fatal error:", error);
180
+ process.exit(1);
181
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "bcap-x402-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Blockchain Capital's x402 payment-gated tweet API",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ARAIDevHub/bcap-x402-backend",
10
+ "directory": "mcp"
11
+ },
12
+ "keywords": ["mcp", "x402", "blockchain", "payments", "twitter"],
13
+ "bin": {
14
+ "bcap-x402-mcp": "./build/index.js"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc && chmod 755 build/index.js",
18
+ "prepublishOnly": "npm run build",
19
+ "start": "node build/index.js"
20
+ },
21
+ "files": ["build"],
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.27.0",
24
+ "@x402/axios": "^2.6.0",
25
+ "@x402/evm": "^2.6.0",
26
+ "@x402/svm": "^2.6.0",
27
+ "@solana/signers": "^5.5.0",
28
+ "@scure/base": "^1.2.6",
29
+ "axios": "^1.13.2",
30
+ "viem": "^2.39.0",
31
+ "zod": "^3.24.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "typescript": "^5.7.0"
36
+ }
37
+ }