@openpump/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.
- package/README.md +201 -0
- package/dist/api-key-auth-VAQIKP2Q.js +9 -0
- package/dist/chunk-J3BWHCCH.js +1691 -0
- package/dist/chunk-P5IDZOR3.js +67 -0
- package/dist/cli/init.js +322 -0
- package/dist/index.js +62 -0
- package/dist/server-MDSOEEF3.js +7 -0
- package/dist/server-http.js +152 -0
- package/package.json +63 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/middleware/api-key-auth.ts
|
|
4
|
+
var UNAUTHORIZED_RESPONSE = {
|
|
5
|
+
jsonrpc: "2.0",
|
|
6
|
+
error: { code: -32001, message: "Unauthorized: Invalid API key" },
|
|
7
|
+
id: null
|
|
8
|
+
};
|
|
9
|
+
function extractRawKey(req) {
|
|
10
|
+
const xApiKey = req.headers["x-api-key"];
|
|
11
|
+
if (typeof xApiKey === "string") return xApiKey;
|
|
12
|
+
const auth = req.headers["authorization"];
|
|
13
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
14
|
+
return auth.slice(7);
|
|
15
|
+
}
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
async function validateViaRestApi(apiKey, apiBaseUrl) {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`${apiBaseUrl}/api/auth/validate`, {
|
|
21
|
+
method: "GET",
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${apiKey}`
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) return null;
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
return {
|
|
29
|
+
userId: data.userId,
|
|
30
|
+
apiKeyId: data.apiKeyId ?? "",
|
|
31
|
+
scopes: data.scopes,
|
|
32
|
+
wallets: data.wallets.map((w, i) => ({
|
|
33
|
+
id: w.id,
|
|
34
|
+
publicKey: w.publicKey,
|
|
35
|
+
label: w.label,
|
|
36
|
+
index: w.index ?? i
|
|
37
|
+
})),
|
|
38
|
+
apiKey
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function createApiKeyMiddleware(apiBaseUrl) {
|
|
45
|
+
return async function apiKeyMiddleware(req, res, next) {
|
|
46
|
+
const rawKey = extractRawKey(req);
|
|
47
|
+
if (!rawKey?.startsWith("op_sk_live_")) {
|
|
48
|
+
res.status(401).json(UNAUTHORIZED_RESPONSE);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const userContext = await validateViaRestApi(rawKey, apiBaseUrl);
|
|
52
|
+
if (!userContext) {
|
|
53
|
+
res.status(401).json(UNAUTHORIZED_RESPONSE);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
req.userContext = userContext;
|
|
57
|
+
next();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function validateApiKey(apiKey, apiBaseUrl) {
|
|
61
|
+
return validateViaRestApi(apiKey, apiBaseUrl);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
createApiKeyMiddleware,
|
|
66
|
+
validateApiKey
|
|
67
|
+
};
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/init.ts
|
|
4
|
+
import * as p2 from "@clack/prompts";
|
|
5
|
+
import color from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/cli/config.ts
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
function getClaudeDesktopConfigPath() {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
switch (process.platform) {
|
|
15
|
+
case "darwin": {
|
|
16
|
+
return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
17
|
+
}
|
|
18
|
+
case "win32": {
|
|
19
|
+
return path.join(
|
|
20
|
+
process.env["APPDATA"] ?? path.join(home, "AppData", "Roaming"),
|
|
21
|
+
"Claude",
|
|
22
|
+
"claude_desktop_config.json"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
default: {
|
|
26
|
+
return path.join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function getCursorConfigPath() {
|
|
31
|
+
return path.join(os.homedir(), ".cursor", "mcp.json");
|
|
32
|
+
}
|
|
33
|
+
function detectClients() {
|
|
34
|
+
const clients = [];
|
|
35
|
+
const desktopPath = getClaudeDesktopConfigPath();
|
|
36
|
+
const desktopDir = path.dirname(desktopPath);
|
|
37
|
+
const desktopDirExists = fs.existsSync(desktopDir);
|
|
38
|
+
const desktopFileExists = fs.existsSync(desktopPath);
|
|
39
|
+
if (desktopDirExists || desktopFileExists) {
|
|
40
|
+
clients.push({
|
|
41
|
+
type: "claude-desktop",
|
|
42
|
+
name: "Claude Desktop",
|
|
43
|
+
configPath: desktopPath,
|
|
44
|
+
exists: desktopFileExists
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const cursorPath = getCursorConfigPath();
|
|
48
|
+
const cursorDir = path.dirname(cursorPath);
|
|
49
|
+
const cursorDirExists = fs.existsSync(cursorDir);
|
|
50
|
+
const cursorFileExists = fs.existsSync(cursorPath);
|
|
51
|
+
if (cursorDirExists || cursorFileExists) {
|
|
52
|
+
clients.push({
|
|
53
|
+
type: "cursor",
|
|
54
|
+
name: "Cursor",
|
|
55
|
+
configPath: cursorPath,
|
|
56
|
+
exists: cursorFileExists
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
execSync("claude --version", { stdio: "ignore" });
|
|
61
|
+
clients.push({
|
|
62
|
+
type: "claude-code",
|
|
63
|
+
name: "Claude Code",
|
|
64
|
+
configPath: null,
|
|
65
|
+
exists: true
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
return clients;
|
|
70
|
+
}
|
|
71
|
+
function buildServerEntry(apiKey) {
|
|
72
|
+
return {
|
|
73
|
+
command: "npx",
|
|
74
|
+
args: ["-y", "@openpump/mcp@latest"],
|
|
75
|
+
env: {
|
|
76
|
+
OPENPUMP_API_KEY: apiKey
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function readConfigFile(configPath) {
|
|
81
|
+
if (!fs.existsSync(configPath)) {
|
|
82
|
+
return { mcpServers: {} };
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
86
|
+
if (!raw.trim()) {
|
|
87
|
+
return { mcpServers: {} };
|
|
88
|
+
}
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
91
|
+
return { mcpServers: {} };
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
} catch {
|
|
95
|
+
return { mcpServers: {} };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function mergeServerEntry(config, apiKey) {
|
|
99
|
+
const mcpServers = typeof config["mcpServers"] === "object" && config["mcpServers"] !== null && !Array.isArray(config["mcpServers"]) ? { ...config["mcpServers"] } : {};
|
|
100
|
+
const alreadyExists = "openpump" in mcpServers;
|
|
101
|
+
mcpServers["openpump"] = buildServerEntry(apiKey);
|
|
102
|
+
return {
|
|
103
|
+
config: { ...config, mcpServers },
|
|
104
|
+
alreadyExists
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function writeConfigFile(configPath, config) {
|
|
108
|
+
const dir = path.dirname(configPath);
|
|
109
|
+
if (!fs.existsSync(dir)) {
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
113
|
+
}
|
|
114
|
+
function writeClientConfigs(clients, apiKey) {
|
|
115
|
+
const outcomes = [];
|
|
116
|
+
for (const client of clients) {
|
|
117
|
+
try {
|
|
118
|
+
if (client.type === "claude-code") {
|
|
119
|
+
execSync(
|
|
120
|
+
"claude mcp add openpump --transport stdio -- npx -y @openpump/mcp@latest",
|
|
121
|
+
{
|
|
122
|
+
stdio: "ignore",
|
|
123
|
+
env: { ...process.env, OPENPUMP_API_KEY: apiKey }
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
outcomes.push({
|
|
127
|
+
clientName: client.name,
|
|
128
|
+
success: true,
|
|
129
|
+
message: "Configured via `claude mcp add`"
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!client.configPath) {
|
|
134
|
+
outcomes.push({
|
|
135
|
+
clientName: client.name,
|
|
136
|
+
success: false,
|
|
137
|
+
message: "No config path available"
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const existing = readConfigFile(client.configPath);
|
|
142
|
+
const { config: merged } = mergeServerEntry(existing, apiKey);
|
|
143
|
+
writeConfigFile(client.configPath, merged);
|
|
144
|
+
outcomes.push({
|
|
145
|
+
clientName: client.name,
|
|
146
|
+
success: true,
|
|
147
|
+
message: `Updated ${client.configPath}`
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
outcomes.push({
|
|
151
|
+
clientName: client.name,
|
|
152
|
+
success: false,
|
|
153
|
+
message: `Failed: ${error instanceof Error ? error.message : String(error)}`
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return outcomes;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/cli/prompts.ts
|
|
161
|
+
import * as p from "@clack/prompts";
|
|
162
|
+
function validateApiKey(value) {
|
|
163
|
+
if (!value.trim()) return "API key is required";
|
|
164
|
+
if (!value.startsWith("op_sk_live_"))
|
|
165
|
+
return 'API key must start with "op_sk_live_"';
|
|
166
|
+
if (value.length < 20) return "API key appears too short";
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
function validateApiKeyPrompt(value) {
|
|
170
|
+
return validateApiKey(value ?? "");
|
|
171
|
+
}
|
|
172
|
+
async function runPrompts(detected, flagApiKey) {
|
|
173
|
+
let apiKey;
|
|
174
|
+
if (flagApiKey) {
|
|
175
|
+
const error = validateApiKey(flagApiKey);
|
|
176
|
+
if (error) {
|
|
177
|
+
p.log.error(`Invalid --api-key: ${error}`);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
apiKey = flagApiKey;
|
|
181
|
+
p.log.info("Using API key from --api-key flag");
|
|
182
|
+
} else {
|
|
183
|
+
const keyResult = await p.text({
|
|
184
|
+
message: "Enter your OpenPump API key:",
|
|
185
|
+
placeholder: "op_sk_live_...",
|
|
186
|
+
validate: validateApiKeyPrompt
|
|
187
|
+
});
|
|
188
|
+
if (p.isCancel(keyResult)) {
|
|
189
|
+
p.cancel("Setup cancelled.");
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
apiKey = keyResult;
|
|
193
|
+
}
|
|
194
|
+
const clientOptions = detected.map((c) => ({
|
|
195
|
+
value: c.type,
|
|
196
|
+
label: c.name,
|
|
197
|
+
hint: c.exists ? "config file found" : "directory found, will create config"
|
|
198
|
+
}));
|
|
199
|
+
const selectedTypes = await p.multiselect({
|
|
200
|
+
message: "Which clients do you want to configure?",
|
|
201
|
+
options: clientOptions,
|
|
202
|
+
required: true
|
|
203
|
+
});
|
|
204
|
+
if (p.isCancel(selectedTypes)) {
|
|
205
|
+
p.cancel("Setup cancelled.");
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const selectedClients = [];
|
|
209
|
+
for (const clientType of selectedTypes) {
|
|
210
|
+
const client = detected.find((c) => c.type === clientType);
|
|
211
|
+
if (!client) continue;
|
|
212
|
+
let overwrite = false;
|
|
213
|
+
if (client.configPath && client.exists) {
|
|
214
|
+
const existing = readConfigFile(client.configPath);
|
|
215
|
+
const mcpServers = existing["mcpServers"];
|
|
216
|
+
if (typeof mcpServers === "object" && mcpServers !== null && "openpump" in mcpServers) {
|
|
217
|
+
const confirmResult = await p.confirm({
|
|
218
|
+
message: `${client.name} already has an OpenPump entry. Overwrite?`
|
|
219
|
+
});
|
|
220
|
+
if (p.isCancel(confirmResult)) {
|
|
221
|
+
p.cancel("Setup cancelled.");
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
if (!confirmResult) {
|
|
225
|
+
p.log.info(`Skipping ${client.name}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
overwrite = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
selectedClients.push({
|
|
232
|
+
type: client.type,
|
|
233
|
+
name: client.name,
|
|
234
|
+
configPath: client.configPath,
|
|
235
|
+
overwrite
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (selectedClients.length === 0) {
|
|
239
|
+
p.log.warn("No clients selected for configuration.");
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return { apiKey, clients: selectedClients };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/cli/init.ts
|
|
246
|
+
function parseArgs(argv) {
|
|
247
|
+
const args = argv.slice(2);
|
|
248
|
+
let apiKey;
|
|
249
|
+
let help = false;
|
|
250
|
+
for (let i = 0; i < args.length; i++) {
|
|
251
|
+
const arg = args[i];
|
|
252
|
+
if (arg === "--help" || arg === "-h") {
|
|
253
|
+
help = true;
|
|
254
|
+
} else if (arg === "--api-key" && args[i + 1]) {
|
|
255
|
+
apiKey = args[i + 1];
|
|
256
|
+
i++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { apiKey, help };
|
|
260
|
+
}
|
|
261
|
+
function printHelp() {
|
|
262
|
+
console.log(`
|
|
263
|
+
${color.bold("@openpump/mcp init")} -- Configure MCP clients for OpenPump
|
|
264
|
+
|
|
265
|
+
${color.dim("Usage:")}
|
|
266
|
+
npx @openpump/mcp init [options]
|
|
267
|
+
|
|
268
|
+
${color.dim("Options:")}
|
|
269
|
+
--api-key <key> OpenPump API key (op_sk_live_...)
|
|
270
|
+
--help, -h Show this help message
|
|
271
|
+
|
|
272
|
+
${color.dim("Supported Clients:")}
|
|
273
|
+
- Claude Desktop (macOS, Windows, Linux)
|
|
274
|
+
- Cursor
|
|
275
|
+
- Claude Code (via claude mcp add)
|
|
276
|
+
|
|
277
|
+
${color.dim("Get your API key at:")}
|
|
278
|
+
https://openpump.io/dashboard/api-keys
|
|
279
|
+
`);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const { apiKey: flagApiKey, help } = parseArgs(process.argv);
|
|
283
|
+
if (help) {
|
|
284
|
+
printHelp();
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
p2.intro(color.bgCyan(color.black(" @openpump/mcp init ")));
|
|
288
|
+
const detected = detectClients();
|
|
289
|
+
if (detected.length === 0) {
|
|
290
|
+
p2.log.warn("No supported MCP clients detected on this system.");
|
|
291
|
+
p2.log.info("Supported clients: Claude Desktop, Cursor, Claude Code");
|
|
292
|
+
p2.outro("Install a supported client and try again.");
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
p2.log.info(`Found ${detected.length.toString()} MCP client(s): ${detected.map((c) => c.name).join(", ")}`);
|
|
296
|
+
const result = await runPrompts(detected, flagApiKey);
|
|
297
|
+
if (!result) {
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
300
|
+
const s = p2.spinner();
|
|
301
|
+
s.start("Writing configuration files...");
|
|
302
|
+
const outcomes = writeClientConfigs(result.clients, result.apiKey);
|
|
303
|
+
s.stop("Configuration complete!");
|
|
304
|
+
for (const outcome of outcomes) {
|
|
305
|
+
if (outcome.success) {
|
|
306
|
+
p2.log.success(`${outcome.clientName}: ${outcome.message}`);
|
|
307
|
+
} else {
|
|
308
|
+
p2.log.error(`${outcome.clientName}: ${outcome.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const configuredClients = outcomes.filter((o) => o.success).map((o) => o.clientName);
|
|
312
|
+
if (configuredClients.length > 0) {
|
|
313
|
+
p2.outro(
|
|
314
|
+
`Restart ${configuredClients.join(", ")} to activate. Problems? ${color.underline(color.cyan("https://openpump.io/docs/mcp"))}`
|
|
315
|
+
);
|
|
316
|
+
} else {
|
|
317
|
+
p2.outro("No clients were configured. Run again to retry.");
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
p2.log.error(String(error));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
if (process.argv[2] === "init") {
|
|
5
|
+
await import("./cli/init.js");
|
|
6
|
+
} else {
|
|
7
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
+
const { createMcpServer } = await import("./server-MDSOEEF3.js");
|
|
9
|
+
const { validateApiKey } = await import("./api-key-auth-VAQIKP2Q.js");
|
|
10
|
+
const API_KEY = process.env["OPENPUMP_API_KEY"] ?? "";
|
|
11
|
+
const API_BASE_URL = process.env["OPENPUMP_API_URL"] ?? "https://openpump.io";
|
|
12
|
+
if (!API_KEY) {
|
|
13
|
+
console.error("ERROR: OPENPUMP_API_KEY environment variable is required.");
|
|
14
|
+
console.error("");
|
|
15
|
+
console.error("Set it to your OpenPump API key (op_sk_live_...) and try again.");
|
|
16
|
+
console.error("");
|
|
17
|
+
console.error("Example:");
|
|
18
|
+
console.error(" OPENPUMP_API_KEY=op_sk_live_abc123 npx @openpump/mcp");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
if (!API_KEY.startsWith("op_sk_live_")) {
|
|
22
|
+
console.error('ERROR: OPENPUMP_API_KEY must start with "op_sk_live_".');
|
|
23
|
+
console.error("");
|
|
24
|
+
console.error(`Received: "${API_KEY.slice(0, 12)}..."`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
console.error("Validating API key...");
|
|
28
|
+
const userContext = await validateApiKey(API_KEY, API_BASE_URL);
|
|
29
|
+
if (!userContext) {
|
|
30
|
+
console.error("ERROR: API key validation failed.");
|
|
31
|
+
console.error("");
|
|
32
|
+
console.error("Possible causes:");
|
|
33
|
+
console.error(" - Invalid or expired API key");
|
|
34
|
+
console.error(" - API server unreachable at " + API_BASE_URL);
|
|
35
|
+
console.error(" - Network connectivity issue");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
console.error(`Authenticated as user ${userContext.userId} with ${userContext.wallets.length.toString()} wallet(s).`);
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${API_BASE_URL}/api/wallets`, {
|
|
41
|
+
headers: { Authorization: `Bearer ${API_KEY}` }
|
|
42
|
+
});
|
|
43
|
+
if (res.ok) {
|
|
44
|
+
const body = await res.json();
|
|
45
|
+
const list = body.data ?? [];
|
|
46
|
+
const balanceMap = new Map(
|
|
47
|
+
list.filter((w) => w.solBalance != null).map((w) => [w.id, w.solBalance])
|
|
48
|
+
);
|
|
49
|
+
if (balanceMap.size > 0) {
|
|
50
|
+
for (const wallet of userContext.wallets) {
|
|
51
|
+
const bal = balanceMap.get(wallet.id);
|
|
52
|
+
if (bal !== void 0) wallet.solBalance = bal;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
const server = createMcpServer(userContext, API_BASE_URL);
|
|
59
|
+
const transport = new StdioServerTransport();
|
|
60
|
+
await server.connect(transport);
|
|
61
|
+
console.error("OpenPump MCP server running on stdio transport.");
|
|
62
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createMcpServer
|
|
4
|
+
} from "./chunk-J3BWHCCH.js";
|
|
5
|
+
import {
|
|
6
|
+
createApiKeyMiddleware
|
|
7
|
+
} from "./chunk-P5IDZOR3.js";
|
|
8
|
+
|
|
9
|
+
// src/server-http.ts
|
|
10
|
+
import express from "express";
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
+
var API_BASE_URL = process.env["OPENPUMP_API_URL"] ?? "https://openpump.io";
|
|
14
|
+
var app = express();
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
17
|
+
var SESSION_TTL_HOURS = Number(process.env["MCP_SESSION_TTL_HOURS"] ?? 4);
|
|
18
|
+
var SESSION_TTL_MS = SESSION_TTL_HOURS * 60 * 60 * 1e3;
|
|
19
|
+
setInterval(() => {
|
|
20
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
21
|
+
for (const [id, session] of sessions) {
|
|
22
|
+
if (session.lastActivity < cutoff) {
|
|
23
|
+
sessions.delete(id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}, 6e4).unref();
|
|
27
|
+
async function enrichWalletBalances(userContext) {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${API_BASE_URL}/api/wallets`, {
|
|
30
|
+
headers: { Authorization: `Bearer ${userContext.apiKey}` }
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) return userContext;
|
|
33
|
+
const body = await res.json();
|
|
34
|
+
const list = body.data ?? [];
|
|
35
|
+
const balanceMap = new Map(
|
|
36
|
+
list.filter((w) => w.solBalance != null).map((w) => [w.id, w.solBalance])
|
|
37
|
+
);
|
|
38
|
+
if (balanceMap.size === 0) return userContext;
|
|
39
|
+
return {
|
|
40
|
+
...userContext,
|
|
41
|
+
wallets: userContext.wallets.map((w) => ({
|
|
42
|
+
...w,
|
|
43
|
+
solBalance: balanceMap.get(w.id) ?? w.solBalance
|
|
44
|
+
}))
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return userContext;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
app.use("/mcp", createApiKeyMiddleware(API_BASE_URL));
|
|
51
|
+
app.all("/mcp", async (req, res) => {
|
|
52
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
53
|
+
const sessionIdStr = typeof sessionId === "string" ? sessionId : void 0;
|
|
54
|
+
const userContext = req.userContext;
|
|
55
|
+
if (!userContext) {
|
|
56
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (req.method === "POST" && !sessionIdStr) {
|
|
60
|
+
const enrichedContext = await enrichWalletBalances(userContext);
|
|
61
|
+
const newSessionId = randomUUID();
|
|
62
|
+
const transport = new StreamableHTTPServerTransport({
|
|
63
|
+
sessionIdGenerator: () => newSessionId,
|
|
64
|
+
enableDnsRebindingProtection: false
|
|
65
|
+
});
|
|
66
|
+
const mcpServer = createMcpServer(enrichedContext, API_BASE_URL);
|
|
67
|
+
await mcpServer.connect(transport);
|
|
68
|
+
sessions.set(newSessionId, { transport, lastActivity: Date.now() });
|
|
69
|
+
await transport.handleRequest(req, res, req.body);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!sessionIdStr) {
|
|
73
|
+
res.status(400).json({ error: "Missing mcp-session-id header" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let session = sessions.get(sessionIdStr);
|
|
77
|
+
if (!session) {
|
|
78
|
+
if (req.method !== "POST") {
|
|
79
|
+
res.status(404).json({ error: "Session not found or expired" });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const enrichedContext = await enrichWalletBalances(userContext);
|
|
83
|
+
const recoveredTransport = new StreamableHTTPServerTransport({
|
|
84
|
+
sessionIdGenerator: () => sessionIdStr,
|
|
85
|
+
enableDnsRebindingProtection: false
|
|
86
|
+
});
|
|
87
|
+
const recoveredServer = createMcpServer(enrichedContext, API_BASE_URL);
|
|
88
|
+
await recoveredServer.connect(recoveredTransport);
|
|
89
|
+
const incomingMethod = req.body?.method;
|
|
90
|
+
if (incomingMethod !== "initialize") {
|
|
91
|
+
const syntheticInitBody = {
|
|
92
|
+
jsonrpc: "2.0",
|
|
93
|
+
id: "__recover__",
|
|
94
|
+
method: "initialize",
|
|
95
|
+
params: {
|
|
96
|
+
protocolVersion: "2024-11-05",
|
|
97
|
+
capabilities: {},
|
|
98
|
+
clientInfo: { name: "session-recover", version: "1.0" }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const noopSink = {
|
|
102
|
+
statusCode: 200,
|
|
103
|
+
headersSent: false,
|
|
104
|
+
setHeader: () => noopSink,
|
|
105
|
+
getHeader: () => void 0,
|
|
106
|
+
removeHeader: () => noopSink,
|
|
107
|
+
writeHead: () => {
|
|
108
|
+
noopSink.headersSent = true;
|
|
109
|
+
return noopSink;
|
|
110
|
+
},
|
|
111
|
+
write: () => true,
|
|
112
|
+
end: () => {
|
|
113
|
+
noopSink.headersSent = true;
|
|
114
|
+
return noopSink;
|
|
115
|
+
},
|
|
116
|
+
status: () => noopSink,
|
|
117
|
+
json: () => noopSink,
|
|
118
|
+
send: () => noopSink,
|
|
119
|
+
on: () => noopSink,
|
|
120
|
+
once: () => noopSink,
|
|
121
|
+
off: () => noopSink,
|
|
122
|
+
emit: () => true,
|
|
123
|
+
flushHeaders: () => {
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
await recoveredTransport.handleRequest(
|
|
128
|
+
req,
|
|
129
|
+
noopSink,
|
|
130
|
+
syntheticInitBody
|
|
131
|
+
);
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
session = { transport: recoveredTransport, lastActivity: Date.now() };
|
|
136
|
+
sessions.set(sessionIdStr, session);
|
|
137
|
+
await recoveredTransport.handleRequest(req, res, req.body);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
session.lastActivity = Date.now();
|
|
141
|
+
if (req.method === "DELETE") {
|
|
142
|
+
sessions.delete(sessionIdStr);
|
|
143
|
+
res.status(200).end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
147
|
+
});
|
|
148
|
+
var PORT = Number(process.env["PORT"] ?? 3001);
|
|
149
|
+
app.listen(PORT, () => {
|
|
150
|
+
console.error(`OpenPump MCP server running on http://localhost:${PORT.toString()}/mcp`);
|
|
151
|
+
console.error(`API base URL: ${API_BASE_URL}`);
|
|
152
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openpump/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "OpenPump MCP server for Solana token operations on pump.fun - works with Claude Desktop, Cursor, and any MCP client",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/openpump/openpump",
|
|
11
|
+
"directory": "packages/mcp-client"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://openpump.io/docs/mcp",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"solana",
|
|
21
|
+
"pump-fun",
|
|
22
|
+
"ai",
|
|
23
|
+
"claude",
|
|
24
|
+
"cursor"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"openpump-mcp": "./dist/index.js",
|
|
28
|
+
"@openpump/mcp": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": "./dist/index.js",
|
|
36
|
+
"./http": "./dist/server-http.js"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsx watch --env-file=../../.env src/index.ts",
|
|
41
|
+
"start": "node dist/index.js",
|
|
42
|
+
"start:http": "node dist/server-http.js",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"lint": "eslint src/",
|
|
46
|
+
"prepublishOnly": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@clack/prompts": "^1.1.0",
|
|
50
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
51
|
+
"express": "^4.18.0",
|
|
52
|
+
"picocolors": "^1.1.1",
|
|
53
|
+
"zod": "^4.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@openpump/typescript-config": "workspace:*",
|
|
57
|
+
"@types/express": "^4.17.21",
|
|
58
|
+
"tsup": "^8.0.0",
|
|
59
|
+
"tsx": "^4.7.0",
|
|
60
|
+
"typescript": "^5.3.0",
|
|
61
|
+
"vitest": "^1.2.0"
|
|
62
|
+
}
|
|
63
|
+
}
|