mcp-server-madeonsol 1.27.1 → 2.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/LICENSE +21 -21
- package/README.md +391 -371
- package/dist/http-security.d.ts +14 -0
- package/dist/http-security.js +180 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +31 -49
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/glama.json +458 -461
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
export declare const MAX_BODY_BYTES: number;
|
|
3
|
+
export declare const BODY_TIMEOUT_MS = 10000;
|
|
4
|
+
export declare const MAX_ACTIVE_REQUESTS = 16;
|
|
5
|
+
export interface HttpConfig {
|
|
6
|
+
host: "127.0.0.1" | "::1";
|
|
7
|
+
port: number;
|
|
8
|
+
allowedHosts: string[];
|
|
9
|
+
tokenDigest: Buffer;
|
|
10
|
+
}
|
|
11
|
+
export declare function readHttpConfig(env?: NodeJS.ProcessEnv): HttpConfig;
|
|
12
|
+
/** Bounded body ingestion; callers must authenticate before invoking this. */
|
|
13
|
+
export declare function readJsonBody(req: IncomingMessage, maxBytes?: number, timeoutMs?: number): Promise<unknown>;
|
|
14
|
+
export declare function createPrivateHttpServer(config: HttpConfig, handle: (req: IncomingMessage, res: ServerResponse, body?: unknown) => Promise<void>): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Keep this standalone-package helper identical in both MCP packages.
|
|
2
|
+
// HTTP is a private, single-operator adapter. Public/multi-user hosting is
|
|
3
|
+
// intentionally unsupported until credentials can be scoped per principal.
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
6
|
+
export const MAX_BODY_BYTES = 256 * 1024;
|
|
7
|
+
export const BODY_TIMEOUT_MS = 10_000;
|
|
8
|
+
export const MAX_ACTIVE_REQUESTS = 16;
|
|
9
|
+
export function readHttpConfig(env = process.env) {
|
|
10
|
+
const host = env.HOST || "127.0.0.1";
|
|
11
|
+
if (host !== "127.0.0.1" && host !== "::1") {
|
|
12
|
+
throw new Error("HTTP MCP requires HOST=127.0.0.1 or ::1; public binding is unsupported. Use stdio for local clients.");
|
|
13
|
+
}
|
|
14
|
+
const portText = env.PORT || "3100";
|
|
15
|
+
const port = Number(portText);
|
|
16
|
+
if (!/^\d+$/.test(portText) || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
17
|
+
throw new Error("HTTP MCP requires a valid PORT between 1 and 65535.");
|
|
18
|
+
}
|
|
19
|
+
if (env.SVM_PRIVATE_KEY || env.RHC_PAYER_KEY) {
|
|
20
|
+
throw new Error("HTTP MCP cannot expose a payment signer. Remove wallet credentials or use stdio.");
|
|
21
|
+
}
|
|
22
|
+
if (!env.MADEONSOL_API_KEY?.startsWith("msk_")) {
|
|
23
|
+
throw new Error("HTTP MCP requires the operator's MADEONSOL_API_KEY.");
|
|
24
|
+
}
|
|
25
|
+
const token = env.MCP_HTTP_TOKEN || "";
|
|
26
|
+
if (!/^[A-Za-z0-9_-]{32,256}$/.test(token) || token === env.MADEONSOL_API_KEY) {
|
|
27
|
+
throw new Error("HTTP MCP requires a separate random MCP_HTTP_TOKEN (32–256 base64url characters).");
|
|
28
|
+
}
|
|
29
|
+
const authority = host === "::1" ? `[::1]:${port}` : `${host}:${port}`;
|
|
30
|
+
return {
|
|
31
|
+
host, port, allowedHosts: [authority, `localhost:${port}`],
|
|
32
|
+
tokenDigest: createHash("sha256").update(token).digest(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
class HttpFailure extends Error {
|
|
36
|
+
status;
|
|
37
|
+
constructor(status, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.status = status;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function fail(res, status, message) {
|
|
43
|
+
if (res.headersSent) {
|
|
44
|
+
res.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
res.writeHead(status, {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
"Cache-Control": "no-store",
|
|
50
|
+
"Connection": "close",
|
|
51
|
+
...(status === 401 ? { "WWW-Authenticate": 'Bearer realm="private-mcp"' } : {}),
|
|
52
|
+
});
|
|
53
|
+
res.end(JSON.stringify({ error: message }));
|
|
54
|
+
}
|
|
55
|
+
function headerCount(req, name) {
|
|
56
|
+
let count = 0;
|
|
57
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
58
|
+
if (req.rawHeaders[i].toLowerCase() === name)
|
|
59
|
+
count++;
|
|
60
|
+
}
|
|
61
|
+
return count;
|
|
62
|
+
}
|
|
63
|
+
function authorize(req, res, config) {
|
|
64
|
+
const remote = req.socket.remoteAddress;
|
|
65
|
+
if ((remote !== "127.0.0.1" && remote !== "::1") ||
|
|
66
|
+
headerCount(req, "host") !== 1 || !config.allowedHosts.includes(req.headers.host || "")) {
|
|
67
|
+
fail(res, 403, "Forbidden host");
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// Native local MCP clients do not need browser origins, CORS or proxy headers.
|
|
71
|
+
if (req.headers.origin !== undefined || Object.keys(req.headers).some(h => h === "forwarded" || h.startsWith("x-forwarded-"))) {
|
|
72
|
+
fail(res, 403, "Browser origins and forwarded requests are unsupported");
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const auth = req.headers.authorization;
|
|
76
|
+
const match = typeof auth === "string" ? /^Bearer ([A-Za-z0-9_-]{32,256})$/i.exec(auth) : null;
|
|
77
|
+
if (headerCount(req, "authorization") !== 1 || !match || !timingSafeEqual(createHash("sha256").update(match[1]).digest(), config.tokenDigest)) {
|
|
78
|
+
fail(res, 401, "Unauthorized");
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/** Bounded body ingestion; callers must authenticate before invoking this. */
|
|
84
|
+
export function readJsonBody(req, maxBytes = MAX_BODY_BYTES, timeoutMs = BODY_TIMEOUT_MS) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let size = 0;
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
req.off("data", data);
|
|
91
|
+
req.off("end", end);
|
|
92
|
+
req.off("aborted", aborted);
|
|
93
|
+
req.off("error", error);
|
|
94
|
+
};
|
|
95
|
+
const stop = (reason) => { cleanup(); req.pause(); reject(reason); };
|
|
96
|
+
const timer = setTimeout(() => stop(new HttpFailure(408, "Request body timed out")), timeoutMs);
|
|
97
|
+
const data = (chunk) => {
|
|
98
|
+
size += chunk.length;
|
|
99
|
+
if (size > maxBytes) {
|
|
100
|
+
stop(new HttpFailure(413, "Request body too large"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
};
|
|
105
|
+
const end = () => {
|
|
106
|
+
cleanup();
|
|
107
|
+
try {
|
|
108
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
reject(new HttpFailure(400, "Invalid JSON"));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const aborted = () => stop(new HttpFailure(400, "Request aborted"));
|
|
115
|
+
const error = () => stop(new HttpFailure(400, "Request failed"));
|
|
116
|
+
req.on("data", data);
|
|
117
|
+
req.once("end", end);
|
|
118
|
+
req.once("aborted", aborted);
|
|
119
|
+
req.once("error", error);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function createPrivateHttpServer(config, handle) {
|
|
123
|
+
let active = 0;
|
|
124
|
+
const server = createServer({ maxHeaderSize: 8192 }, (req, res) => {
|
|
125
|
+
void (async () => {
|
|
126
|
+
if (!authorize(req, res, config))
|
|
127
|
+
return;
|
|
128
|
+
const isMcp = req.url === "/mcp";
|
|
129
|
+
const isInfo = req.url === "/health" || req.url === "/.well-known/mcp/server-card.json";
|
|
130
|
+
if (!isMcp && !isInfo) {
|
|
131
|
+
fail(res, 404, "Not found");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const method = isMcp ? "POST" : "GET";
|
|
135
|
+
if (req.method !== method) {
|
|
136
|
+
res.setHeader("Allow", method);
|
|
137
|
+
fail(res, 405, "Method not allowed");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (isMcp && (req.headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/json" ||
|
|
141
|
+
(req.headers["content-encoding"] !== undefined && req.headers["content-encoding"] !== "identity"))) {
|
|
142
|
+
fail(res, 415, "Only uncompressed application/json is supported");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (Number(req.headers["content-length"] || 0) > MAX_BODY_BYTES) {
|
|
146
|
+
fail(res, 413, "Request body too large");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// No sessions are issued: a supplied session id must never become an auth bypass.
|
|
150
|
+
if (req.headers["mcp-session-id"] !== undefined) {
|
|
151
|
+
fail(res, 400, "HTTP MCP is stateless; session identifiers are unsupported");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (active >= MAX_ACTIVE_REQUESTS) {
|
|
155
|
+
fail(res, 503, "Too many active requests");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
active++;
|
|
159
|
+
let released = false;
|
|
160
|
+
const release = () => { if (!released) {
|
|
161
|
+
released = true;
|
|
162
|
+
active--;
|
|
163
|
+
} };
|
|
164
|
+
res.once("close", release);
|
|
165
|
+
res.once("finish", release);
|
|
166
|
+
res.setHeader("Cache-Control", "no-store");
|
|
167
|
+
const body = isMcp ? await readJsonBody(req) : undefined;
|
|
168
|
+
if (!res.destroyed)
|
|
169
|
+
await handle(req, res, body);
|
|
170
|
+
})().catch(error => {
|
|
171
|
+
if (!res.destroyed)
|
|
172
|
+
fail(res, error instanceof HttpFailure ? error.status : 500, error instanceof HttpFailure ? error.message : "Internal server error");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
server.headersTimeout = 10_000;
|
|
176
|
+
server.requestTimeout = 15_000;
|
|
177
|
+
server.keepAliveTimeout = 5_000;
|
|
178
|
+
server.maxRequestsPerSocket = 100;
|
|
179
|
+
return server;
|
|
180
|
+
}
|
package/dist/index.d.ts
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { VERSION } from "./version.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createPrivateHttpServer, readHttpConfig } from "./http-security.js";
|
|
8
8
|
// MCP `initialize` response `instructions` field (ServerOptions.instructions in
|
|
9
9
|
// the SDK) — operational guidance for the calling agent, distinct from the
|
|
10
10
|
// package/marketing description in package.json and glama.json. Left unset
|
|
@@ -14,7 +14,6 @@ const SERVER_INSTRUCTIONS = "Real-time Solana on-chain intelligence: KOL wallet
|
|
|
14
14
|
const BASE_URL = process.env.MADEONSOL_API_URL || "https://madeonsol.com";
|
|
15
15
|
const MADEONSOL_API_KEY = process.env.MADEONSOL_API_KEY; // Native key from madeonsol.com/pricing
|
|
16
16
|
const PRIVATE_KEY = process.env.SVM_PRIVATE_KEY; // x402 micropayments (for AI agents)
|
|
17
|
-
const PORT = parseInt(process.env.PORT || "3100", 10);
|
|
18
17
|
const MODE = process.env.MCP_TRANSPORT || "stdio"; // "stdio" or "http"
|
|
19
18
|
let authMode = "none";
|
|
20
19
|
let paidFetch = fetch;
|
|
@@ -1275,12 +1274,11 @@ function registerTools(server) {
|
|
|
1275
1274
|
});
|
|
1276
1275
|
}
|
|
1277
1276
|
async function main() {
|
|
1277
|
+
const httpConfig = MODE === "http" ? readHttpConfig() : undefined;
|
|
1278
1278
|
await initAuth();
|
|
1279
1279
|
if (MODE === "http") {
|
|
1280
|
-
//
|
|
1281
|
-
const httpServer =
|
|
1282
|
-
const transports = new Map();
|
|
1283
|
-
httpServer.on("request", async (req, res) => {
|
|
1280
|
+
// Configuration is validated before authentication can initialize a signer.
|
|
1281
|
+
const httpServer = createPrivateHttpServer(httpConfig, async (req, res, body) => {
|
|
1284
1282
|
// Health check
|
|
1285
1283
|
if (req.method === "GET" && req.url === "/health") {
|
|
1286
1284
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1292,7 +1290,7 @@ async function main() {
|
|
|
1292
1290
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1293
1291
|
res.end(JSON.stringify({
|
|
1294
1292
|
name: "madeonsol",
|
|
1295
|
-
description: "Solana KOL trading intelligence and deployer analytics. Real-time data from 1,000+ KOL wallets, 15,500+ Pump.fun deployers, 25,000+ scored alpha wallets, copy-trade rules, and wallet tracker.
|
|
1293
|
+
description: "Solana KOL trading intelligence and deployer analytics. Real-time data from 1,000+ KOL wallets, 15,500+ Pump.fun deployers, 25,000+ scored alpha wallets, copy-trade rules, and wallet tracker. Private HTTP access with the operator's MadeOnSol API key (msk_).",
|
|
1296
1294
|
version: VERSION,
|
|
1297
1295
|
tools: [
|
|
1298
1296
|
{ name: "madeonsol_kol_feed", description: "Get real-time Solana KOL trades from 1,000+ tracked wallets." },
|
|
@@ -1390,49 +1388,30 @@ async function main() {
|
|
|
1390
1388
|
}));
|
|
1391
1389
|
return;
|
|
1392
1390
|
}
|
|
1393
|
-
//
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1409
|
-
if (req.method === "GET" && sessionId) {
|
|
1410
|
-
const transport = transports.get(sessionId);
|
|
1411
|
-
if (transport) {
|
|
1412
|
-
await transport.handleRequest(req, res);
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
if (req.method === "DELETE" && sessionId) {
|
|
1417
|
-
const transport = transports.get(sessionId);
|
|
1418
|
-
if (transport) {
|
|
1419
|
-
await transport.handleRequest(req, res);
|
|
1420
|
-
transports.delete(sessionId);
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1391
|
+
// One server/transport per authenticated stateless POST /mcp.
|
|
1392
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1393
|
+
sessionIdGenerator: undefined,
|
|
1394
|
+
enableJsonResponse: true,
|
|
1395
|
+
enableDnsRebindingProtection: true,
|
|
1396
|
+
allowedHosts: httpConfig.allowedHosts,
|
|
1397
|
+
allowedOrigins: [],
|
|
1398
|
+
});
|
|
1399
|
+
const server = new McpServer({ name: "madeonsol", version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
1400
|
+
res.once("close", () => { void server.close().catch(() => { }); });
|
|
1401
|
+
registerTools(server);
|
|
1402
|
+
await server.connect(transport);
|
|
1403
|
+
if (res.destroyed) {
|
|
1404
|
+
await server.close();
|
|
1405
|
+
return;
|
|
1424
1406
|
}
|
|
1425
|
-
|
|
1426
|
-
res.end("Not found");
|
|
1407
|
+
await transport.handleRequest(req, res, body);
|
|
1427
1408
|
});
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
httpServer.listen(PORT, HOST, () => {
|
|
1435
|
-
console.error(`[madeonsol-mcp] HTTP server listening on ${HOST}:${PORT}`);
|
|
1409
|
+
httpServer.on("error", () => {
|
|
1410
|
+
console.error("[madeonsol-mcp] HTTP listener failed");
|
|
1411
|
+
process.exitCode = 1;
|
|
1412
|
+
});
|
|
1413
|
+
httpServer.listen(httpConfig.port, httpConfig.host, () => {
|
|
1414
|
+
console.error(`[madeonsol-mcp] HTTP server listening on ${httpConfig.host}:${httpConfig.port}/mcp (private operator only)`);
|
|
1436
1415
|
});
|
|
1437
1416
|
}
|
|
1438
1417
|
else {
|
|
@@ -1446,5 +1425,8 @@ async function main() {
|
|
|
1446
1425
|
// Only auto-run when executed as a program (CLI / spawned process), not when
|
|
1447
1426
|
// the module is imported by a test for its exported pure helpers.
|
|
1448
1427
|
if (process.env.MADEONSOL_MCP_NO_AUTORUN !== "1") {
|
|
1449
|
-
main().catch(
|
|
1428
|
+
main().catch(error => {
|
|
1429
|
+
console.error(error instanceof Error ? error.message : "MCP startup failed");
|
|
1430
|
+
process.exitCode = 1;
|
|
1431
|
+
});
|
|
1450
1432
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "
|
|
1
|
+
export declare const VERSION = "2.0.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED by ../gen-version.mjs (npm prebuild) from package.json. Do not edit.
|
|
2
|
-
export const VERSION = "
|
|
2
|
+
export const VERSION = "2.0.0";
|