import express, { type Request, type Response } from "express";
import cors from "cors";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { solanaConfig } from "./config.js";
import { solanaTools } from "./tools.js";
async function createSolanaServer() {
const server = new McpServer(
{
name: solanaConfig.mcpServer.name,
version: solanaConfig.mcpServer.version,
},
{
capabilities: {
tools: {},
},
},
);
// Solana API Tools & Custom Tools
for (const t of solanaTools) {
server.registerTool(
t.name,
{
title: t.name,
description: t.description,
inputSchema: t.inputSchema,
},
async (args) => {
const result = await t.callback(args);
return {
content: result.content.map((item) => ({
...item,
type: "text" as const,
})),
};
},
);
}
return server;
}
export async function start() {
const useStreamHttp = process.env.USE_STREAMABLE_HTTP === "true";
const useStdIO = !useStreamHttp;
const port = Number(process.env.PORT || 3000);
const host = process.env.HOST || "0.0.0.0";
const server = await createSolanaServer();
if (useStdIO) {
const transport = new StdioServerTransport();
await server.connect(transport);
// Stdio mode - no console.log to avoid breaking JSON-RPC protocol
// Use console.error for debugging if needed (stderr won't break stdio)
return;
}
const app = express();
app.use(express.json());
app.use(
cors({
origin: "*",
allowedHeaders: ["Content-Type", "mcp-session-id"],
}),
);
app.post("/mcp", async (req: Request, res: Response) => {
try {
const transport: StreamableHTTPServerTransport =
new StreamableHTTPServerTransport({
enableDnsRebindingProtection: true,
sessionIdGenerator: undefined,
});
res.on("close", () => {
console.log("Request closed");
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling MCP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
});
}
}
});
app.get("/mcp", async (req: Request, res: Response) => {
console.log("Received GET MCP request");
res.writeHead(405).end(
JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed.",
},
id: null,
}),
);
});
app.delete("/mcp", async (req: Request, res: Response) => {
console.log("Received DELETE MCP request");
res.writeHead(405).end(
JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed.",
},
id: null,
}),
);
});
app.get("/health", (_req, res) => res.status(200).send("ok"));
app.listen(port, host, () => {
console.log(
`MCP Stateless Streamable HTTP listening on http://${host}:${port}`,
);
console.log(
`Mode: ${process.env.ENVIRONMENT === "TESTNET" ? "Testnet" : "Devnet"}`,
);
});
}