Skip to main content
Glama
http.transport.ts14.6 kB
/** * HTTP Transport Module * Handles HTTP-based MCP server communication with Express */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import cors from "cors"; import type { Request, Response } from "express"; import express, { type Express } from "express"; import { config } from "../../shared/utils/config.util.js"; import { VERSION } from "../../shared/utils/constants.util.js"; import { Logger } from "../../shared/utils/logger.util.js"; const logger = Logger.forContext("transports/http.transport.ts"); export interface HttpTransportConfig { port?: number; mcpEndpoint?: string; } /** * Parse Smithery base64-encoded config parameter * @param configParam Base64-encoded JSON configuration string * @returns Parsed configuration object or null if invalid */ function parseSmitheryConfig( configParam: string, ): Record<string, unknown> | null { try { // URL decode first (in case it's URL-encoded) const urlDecoded = decodeURIComponent(configParam); // Then base64 decode const jsonString = Buffer.from(urlDecoded, "base64").toString("utf-8"); // Parse the JSON return JSON.parse(jsonString); } catch (error) { logger.error("Failed to parse Smithery config", error); return null; } } /** * Parse dot-notation query parameters into nested objects * e.g., "server.host" -> { server: { host: value } } */ function parseQueryConfig( query: Record<string, unknown>, ): Record<string, unknown> { const parsed: Record<string, unknown> = {}; for (const [key, value] of Object.entries(query)) { // Skip the 'config' parameter as it's handled separately for Smithery if (key === "config") { continue; } const parts = key.split("."); let current = parsed; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current)) { current[part] = {}; } current = current[part] as Record<string, unknown>; } current[parts[parts.length - 1]] = value; } return parsed; } /** * Create Express app with MCP endpoint */ function createExpressApp( transport: StreamableHTTPServerTransport, mcpEndpoint: string, port: number, ): Express { const app = express(); // Middleware app.use(cors()); app.use(express.json()); // Health check endpoint app.get("/", (_req: Request, res: Response) => { res.type("html").send(`<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Lokalise MCP Server</title> <link rel="icon" type="image/png" sizes="96x96" href="https://static.lokalise.com/img/favicon/favicon-96x96.png"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; background: linear-gradient(135deg, #0c0c0c 0%, #1a1a1a 100%); color: #e0e0e0; min-height: 100vh; padding: 2rem; line-height: 1.6; } .container { max-width: 800px; margin: 0 auto; background: rgba(0, 0, 0, 0.6); border-radius: 12px; border: 1px solid #333; backdrop-filter: blur(10px); overflow: hidden; } .header { background: linear-gradient(90deg, #00c851 0%, #00ff41 100%); color: #000; padding: 1.5rem 2rem; text-align: center; } .logo { font-size: 0.5rem; line-height: 1; color: #000; margin-bottom: 0.5rem; font-weight: 400; font-family: 'Courier New', monospace; letter-spacing: -0.05em; white-space: pre; } .title { font-size: 1.2rem; font-weight: 700; margin-top: 0.5rem; } .subtitle { font-size: 0.9rem; font-weight: 400; color: rgba(0, 0, 0, 0.8); margin: 0.5rem 0 1rem 0; } .badges { display: flex; justify-content: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; } .badges img { height: 20px; border-radius: 3px; transition: transform 0.2s ease; } .badges img:hover { transform: translateY(-1px); } .content { padding: 2rem; } .section { margin-bottom: 2rem; } .section:last-child { margin-bottom: 0; } h3 { color: #00c851; font-size: 1rem; margin-bottom: 0.8rem; font-weight: 600; } .install-cmd { background: #1a1a1a; border: 1px solid #333; border-radius: 6px; padding: 0.8rem 1rem; font-family: inherit; color: #00ff41; font-size: 0.9rem; margin: 0.5rem 0; user-select: all; cursor: pointer; transition: all 0.2s ease; } .install-cmd:hover { background: #222; border-color: #00c851; } .code-block { background: #0f0f0f; border: 1px solid #2a2a2a; border-radius: 6px; padding: 1rem; font-size: 0.8rem; color: #b0b0b0; margin: 0.5rem 0; overflow-x: auto; font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; line-height: 1.4; white-space: pre; } .url { background: #1a1a1a; color: #00ff41; padding: 0.2rem 0.5rem; border-radius: 4px; user-select: all; cursor: pointer; } a { color: #00c851; text-decoration: none; transition: color 0.2s ease; } a:hover { color: #00ff41; } .steps { list-style: none; counter-reset: step-counter; } .steps li { counter-increment: step-counter; margin-bottom: 0.5rem; position: relative; padding-left: 2rem; } .steps li::before { content: counter(step-counter); position: absolute; left: 0; top: 0; background: #00c851; color: #000; width: 1.5rem; height: 1.5rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 600; } .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1rem; } @media (max-width: 768px) { body { padding: 1rem; } .grid { grid-template-columns: 1fr; gap: 1rem; } .header { padding: 1rem; } .content { padding: 1.5rem; } } </style> </head> <body> <div class="container"> <div class="header"> <div class="logo">@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@ @@@@@@@@@@@ @@@@@@@@@@@@@@@@@< @@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@</div> <div class="title">Lokalise MCP Server v${VERSION}</div> <div class="subtitle">Bring the power of Lokalise to your AI assistant</div> <div class="badges"> <a href="https://smithery.ai/server/@AbdallahAHO/lokalise-mcp" target="_blank"> <img src="https://smithery.ai/badge/@AbdallahAHO/lokalise-mcp" alt="Smithery Badge" /> </a> <a href="https://www.npmjs.com/package/lokalise-mcp" target="_blank"> <img src="https://img.shields.io/npm/v/lokalise-mcp" alt="NPM Version" /> </a> <a href="https://opensource.org/licenses/MIT" target="_blank"> <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /> </a> <a href="https://www.typescriptlang.org/" target="_blank"> <img src="https://img.shields.io/badge/TypeScript-5.0%2B-blue" alt="TypeScript" /> </a> <a href="https://nodejs.org/" target="_blank"> <img src="https://img.shields.io/badge/Node.js-18%2B-green" alt="Node.js" /> </a> </div> </div> <div class="content"> <div class="section"> <h3>🚀 Quick Install</h3> <div class="install-cmd">npx -y @smithery/cli install @AbdallahAHO/lokalise-mcp --client claude</div> </div> <div class="grid"> <div class="section"> <h3>🔧 Manual Setup</h3> <div class="code-block"><span style="color: #666;">{</span> <span style="color: #00c851;">"mcpServers"</span><span style="color: #666;">:</span> <span style="color: #666;">{</span> <span style="color: #00c851;">"lokalise"</span><span style="color: #666;">:</span> <span style="color: #666;">{</span> <span style="color: #00c851;">"command"</span><span style="color: #666;">:</span> <span style="color: #00ff41;">"npx"</span><span style="color: #666;">,</span> <span style="color: #00c851;">"args"</span><span style="color: #666;">:</span> <span style="color: #666;">[</span><span style="color: #00ff41;">"-y"</span><span style="color: #666;">,</span> <span style="color: #00ff41;">"lokalise-mcp"</span><span style="color: #666;">],</span> <span style="color: #00c851;">"env"</span><span style="color: #666;">:</span> <span style="color: #666;">{</span> <span style="color: #00c851;">"LOKALISE_API_KEY"</span><span style="color: #666;">:</span> <span style="color: #00ff41;">"your-api-key-here"</span> <span style="color: #666;">}</span> <span style="color: #666;">}</span> <span style="color: #666;">}</span> <span style="color: #666;">}</span></div> </div> <div class="section"> <h3>🌐 Direct Connection</h3> <p>Connect to: <span class="url">http://localhost:${port}/mcp</span></p> <p style="margin-top: 0.5rem; font-size: 0.85rem; color: #888;">Set PORT=${port} in environment</p> </div> </div> <div class="section"> <h3>🔑 Get API Key</h3> <ol class="steps"> <li>Login to <a href="https://app.lokalise.com" target="_blank">Lokalise</a></li> <li>Go to <a href="https://app.lokalise.com/profile#apitokens" target="_blank">Profile → API Tokens</a></li> <li>Click "Generate new token"</li> <li>Copy and configure securely</li> </ol> </div> <div class="section"> <h3>✅ Test</h3> <p>Ask your AI assistant: <em>"Can you list my Lokalise projects?"</em></p> <p style="margin-top: 0.5rem;"><a href="https://github.com/AbdallahAHO/lokalise-mcp" target="_blank">Documentation</a> • <a href="https://smithery.ai/server/@AbdallahAHO/lokalise-mcp" target="_blank">Smithery</a></p> </div> </div> </div> </body> </html>`); }); // MCP endpoint app.all(mcpEndpoint, (req: Request, res: Response) => { // Extract query parameters for Smithery configuration const queryParams = req.query; // Check for Smithery base64-encoded config parameter first if (queryParams.config && typeof queryParams.config === "string") { logger.debug("Received Smithery config parameter", { configLength: queryParams.config.length, }); // Parse the base64-encoded config const smitheryConfig = parseSmitheryConfig(queryParams.config); if (smitheryConfig) { // Set the Smithery configuration (highest priority) config.setSmitheryConfig(queryParams.config); // Force reload configuration to apply the Smithery config config.reload(); logger.info("Configuration loaded from Smithery", { hasApiKey: !!smitheryConfig.LOKALISE_API_KEY, hostname: smitheryConfig.LOKALISE_API_HOSTNAME, debug: smitheryConfig.debug_mode, }); } } else if (Object.keys(queryParams).length > 0) { // Fall back to regular query parameters if no Smithery config logger.debug("Received query parameters", { keys: Object.keys(queryParams), }); // Parse and set configuration with high priority const parsedConfig = parseQueryConfig(queryParams); if (Object.keys(parsedConfig).length > 0) { config.setHttpQueryConfig(parsedConfig); // Force reload configuration to apply the HTTP query parameters config.reload(); logger.info("Configuration reloaded with HTTP query parameters"); } } // Handle MCP request transport.handleRequest(req, res, req.body).catch((err: unknown) => { logger.error("Error in transport.handleRequest", err); if (!res.headersSent) { res.status(500).json({ error: "Internal Server Error", }); } }); }); return app; } /** * Start Express server */ async function startExpressServer(app: Express, port: number): Promise<void> { return new Promise<void>((resolve, reject) => { const server = app.listen(port, () => { logger.info(`HTTP transport listening on http://localhost:${port}/mcp`); resolve(); }); server.on("error", (err) => { reject(err); }); }); } /** * Initialize and connect HTTP transport to MCP server */ export async function initializeHttpTransport( server: McpServer, transportConfig?: HttpTransportConfig, ): Promise<StreamableHTTPServerTransport> { logger.info("Initializing HTTP transport"); const port = transportConfig?.port ?? config.getPort(); const mcpEndpoint = transportConfig?.mcpEndpoint ?? "/mcp"; const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); try { // Connect transport to MCP server await server.connect(transport); // Create and start Express app const app = createExpressApp(transport, mcpEndpoint, port); await startExpressServer(app, port); return transport; } catch (error) { logger.error("Failed to start HTTP transport", error); throw new Error(`HTTP transport initialization failed: ${error}`); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AbdallahAHO/lokalise-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server