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.
@@ -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 { createServer } from "node:http";
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
- // HTTP transport for hosted environments (Smithery, etc.)
1281
- const httpServer = createServer();
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. Supports MadeOnSol API key (msk_) or x402 micropayments.",
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
- // MCP endpoint
1394
- const sessionId = req.headers["mcp-session-id"];
1395
- {
1396
- if (req.method === "POST") {
1397
- let transport = sessionId ? transports.get(sessionId) : undefined;
1398
- if (!transport) {
1399
- transport = new StreamableHTTPServerTransport({
1400
- sessionIdGenerator: undefined,
1401
- });
1402
- const server = new McpServer({ name: "madeonsol", version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
1403
- registerTools(server);
1404
- await server.connect(transport);
1405
- }
1406
- await transport.handleRequest(req, res);
1407
- return;
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
- res.writeHead(404);
1426
- res.end("Not found");
1407
+ await transport.handleRequest(req, res, body);
1427
1408
  });
1428
- // Bind to 127.0.0.1 only — defense in depth. UFW already blocks the port
1429
- // externally, but binding to all interfaces would expose the server to any
1430
- // misconfigured firewall rule. Override with HOST=0.0.0.0 if you ever need
1431
- // to expose it directly (e.g. for hosted environments behind a separate
1432
- // reverse proxy).
1433
- const HOST = process.env.HOST || "127.0.0.1";
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(console.error);
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.27.1";
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 = "1.27.1";
2
+ export const VERSION = "2.0.0";