Skip to main content
Glama
loda-lang

LODA API MCP Server

Official
by loda-lang
index.ts32.1 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; import { randomUUID } from "node:crypto"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, Tool, } from "@modelcontextprotocol/sdk/types.js"; // LODA API Configuration const LODA_API_BASE_URL = process.env.LODA_API_BASE_URL || "https://api.loda-lang.org/v2"; // Type definitions based on the OpenAPI specification interface SequenceDetails { id: string; name: string; terms: string[]; keywords?: string[]; oeisRef?: string | null; } interface ProgramDetails { id: string; name: string; code: string; submitter?: string | null; keywords?: string[]; operations?: string[]; usages?: string[]; } interface Result { status: "success" | "error"; message: string; terms: string[]; } interface ExportResult { status: "success" | "error"; message: string; output: string; } interface Submission { id: string; mode: "add" | "update" | "remove"; type: "program" | "sequence"; content?: string; submitter?: string; } interface SubmissionsResult { session: number; total: number; results: Submission[]; } interface StatsSummary { numSequences: number; numPrograms: number; numFormulas: number; } interface Submitter { name: string; numPrograms: number; } /** * LODA API Client - handles all communication with the LODA API */ class LODAApiClient { private baseUrl: string; constructor(baseUrl: string = LODA_API_BASE_URL) { this.baseUrl = baseUrl; } private async makeRequest(endpoint: string, options: RequestInit = {}): Promise<any> { const url = `${this.baseUrl}${endpoint}`; const defaultOptions: RequestInit = { headers: { 'Accept': 'application/json', 'User-Agent': 'loda-mcp/1.0.0', ...options.headers, }, }; try { const response = await fetch(url, { ...defaultOptions, ...options }); if (!response.ok) { let errorMessage = `HTTP ${response.status}: ${response.statusText}`; try { const errorBody = await response.text(); if (errorBody) { errorMessage += ` - ${errorBody}`; } } catch (e) {} throw new McpError( ErrorCode.InternalError, `LODA API request failed: ${errorMessage}` ); } const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); } else { return await response.text(); } } catch (error) { if (error instanceof McpError) throw error; if (error instanceof Error) { if (error.message.includes('fetch')) { throw new McpError( ErrorCode.InternalError, `Network error: Unable to connect to LODA API at ${this.baseUrl}` ); } throw new McpError(ErrorCode.InternalError, `Request error: ${error.message}`); } throw new McpError(ErrorCode.InternalError, 'Unknown error occurred'); } } async getSequence(id: string): Promise<SequenceDetails> { return this.makeRequest(`/sequences/${id}`); } async searchSequences(q: string, limit?: number, skip?: number, shuffle?: boolean): Promise<{ total: number; results: { id: string; name: string; keywords?: string[] }[] }> { const params = new URLSearchParams({ q }); if (limit !== undefined) params.append('limit', String(limit)); if (skip !== undefined) params.append('skip', String(skip)); if (shuffle !== undefined) params.append('shuffle', String(shuffle)); // The API returns { total, results: [{id, name, keywords?}] } return this.makeRequest(`/sequences/search?${params.toString()}`); } async getProgram(id: string): Promise<ProgramDetails> { return this.makeRequest(`/programs/${id}`); } async searchPrograms(q: string, limit?: number, skip?: number, shuffle?: boolean): Promise<{ total: number; results: { id: string; name: string; keywords?: string[] }[] }> { const params = new URLSearchParams({ q }); if (limit !== undefined) params.append('limit', String(limit)); if (skip !== undefined) params.append('skip', String(skip)); if (shuffle !== undefined) params.append('shuffle', String(shuffle)); // The API returns { total, results: [{id, name, keywords?}] } return this.makeRequest(`/programs/search?${params.toString()}`); } async evalProgram(code: string, t?: number, o?: number): Promise<Result> { const params = new URLSearchParams(); if (t !== undefined) params.append('t', String(t)); if (o !== undefined) params.append('o', String(o)); return this.makeRequest(`/programs/eval${params.size ? '?' + params.toString() : ''}`, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: code, }); } async exportProgram(code: string, format?: string): Promise<ExportResult> { const params = new URLSearchParams(); if (format !== undefined) params.append('format', format); return this.makeRequest(`/programs/export${params.size ? '?' + params.toString() : ''}`, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: code, }); } async getSubmissions(limit?: number, skip?: number, mode?: string, type?: string, submitter?: string): Promise<SubmissionsResult> { const params = new URLSearchParams(); if (limit !== undefined) params.append('limit', String(limit)); if (skip !== undefined) params.append('skip', String(skip)); if (mode !== undefined) params.append('mode', mode); if (type !== undefined) params.append('type', type); if (submitter !== undefined) params.append('submitter', submitter); return this.makeRequest(`/submissions${params.size ? '?' + params.toString() : ''}`); } async createSubmission(submission: Submission): Promise<{ status: string; message: string }> { return this.makeRequest('/submissions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(submission), }); } async getStats(): Promise<StatsSummary> { return this.makeRequest('/stats/summary'); } async getKeywords(): Promise<{ name: string; description: string; numPrograms: number; numSequences: number }[]> { return this.makeRequest('/stats/keywords'); } async getSubmitters(): Promise<Submitter[]> { return this.makeRequest('/stats/submitters'); } async getUsageStats(): Promise<{ id: string; numUsages: number }[]> { return this.makeRequest('/stats/programs/numUsages'); } } /** * LODA MCP Server - provides MCP interface to LODA API */ class LODAMCPServer { private server: Server; private apiClient: LODAApiClient; constructor() { this.server = new Server( { name: "loda-api-server", version: "2.0.0", description: "MCP server for LODA Language API v2 (OpenAPI 3.0.3)" }, { capabilities: { tools: {}, }, } ); this.apiClient = new LODAApiClient(); this.setupToolHandlers(); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_program_details", description: "Retrieve detailed information about a LODA program for an integer sequence. The response includes: id, name, code (plain text), submitter, keywords, operations, formula (if available), and usages (IDs of programs that use this one via seq). The ID must match the sequence (e.g., A000045).", inputSchema: { type: "object", properties: { id: { type: "string", description: "ID of the LODA program (e.g. A000045)" } }, required: ["id"], additionalProperties: false } }, { name: "search_programs", description: "Search for LODA programs using flexible criteria. Supports pagination.\n" + "\nSupported search criteria:\n" + "- Name: Matches tokens in the program name (case-insensitive).\n" + "- ID: Matches tokens in the program ID (e.g., A000045).\n" + "- Keywords: Include keywords by specifying them in the query (e.g., 'core easy'). Exclude keywords by prefixing with a minus sign (e.g., '-hard').\n" + "- Operation Types: Include operation types (opcodes) to match in the LODA program (e.g., `mov add`). Exclude operation types by prefixing with a minus sign (e.g., `-mul`).\n" + "- Submitter: Matches tokens in the submitter's name (case-insensitive).\n" + "- Advanced: All tokens in the query must be present in either the program name or submitter name. Keywords and operation types are handled as described above.\n" + "\nExample queries:\n" + "- 'Fibonacci core' (programs with 'Fibonacci' in the name and the 'core' keyword)\n" + "- 'A000045' (program with ID A000045)" + "- 'Alice' (programs submitted by Alice)\n" + "- '-hard' (exclude programs with the 'hard' keyword)\n" + "- 'bin' (include programs with 'bin' operations)\n", inputSchema: { type: "object", properties: { q: { type: "string", description: "Search query supporting keywords, properties, submitters, and advanced criteria. To require a keyword, include it; to exclude, prefix with '-' (e.g., -core)." }, limit: { type: "number", description: "Maximum number of results to return (pagination limit)", minimum: 1, maximum: 100 }, skip: { type: "number", description: "Number of items to skip before starting to collect the result set (pagination offset)", minimum: 0 }, shuffle: { type: "boolean", description: "If set to true, the search results will be shuffled randomly" } }, required: ["q"], additionalProperties: false } }, { name: "eval_program", description: "Evaluate a LODA program to generate the corresponding integer sequence. The request body should contain the program code in plain text format. Optionally specify the number of terms and offset.", inputSchema: { type: "object", properties: { code: { type: "string", description: "LODA program code in plain text format." }, t: { type: "number", description: "Number of terms to compute" , minimum: 1, maximum: 10000 }, o: { type: "number", description: "The starting index (offset) for evaluating the sequence program. Overrides #offset directive or defaults to 0." } }, required: ["code"], additionalProperties: false } }, { name: "export_program", description: "Export a LODA program to different formats (formula, pari, lean, loda, range). The default format is 'formula'.", inputSchema: { type: "object", properties: { code: { type: "string", description: "LODA program code in plain text format." }, format: { type: "string", description: "Export format: formula, pari, lean, loda, or range. Default is 'formula'.", enum: ["formula", "pari", "lean", "loda", "range"] } }, required: ["code"], additionalProperties: false } }, { name: "get_submissions", description: "Retrieve a paginated list of submissions. Returns all submissions including programs and sequences with support for pagination and filtering by mode (add/update/remove), type (program/sequence), and submitter.", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of results to return (pagination limit)", minimum: 1, maximum: 100 }, skip: { type: "number", description: "Number of items to skip before starting to collect the result set (pagination offset)", minimum: 0 }, mode: { type: "string", description: "Filter submissions by mode", enum: ["add", "update", "remove"] }, type: { type: "string", description: "Filter submissions by object type", enum: ["program", "sequence"] }, submitter: { type: "string", description: "Filter submissions by submitter name" } }, additionalProperties: false } }, { name: "submit", description: "Submit a new program or sequence. Currently only program submissions are supported. Submission modes: add (new program/sequence), update (modify existing), remove (delete). Object types: program (LODA program), sequence (integer sequence - not yet supported). Note: content is not required when mode is 'remove'.", inputSchema: { type: "object", properties: { id: { type: "string", description: "ID of the program or sequence (e.g. A000045)" }, submitter: { type: "string", description: "(Optional) Name of the person submitting" }, content: { type: "string", description: "Content of the submission (program code or sequence data). Not required when mode is 'remove'." }, mode: { type: "string", description: "Type of submission operation", enum: ["add", "update", "remove"] }, type: { type: "string", description: "Type of object being submitted", enum: ["program", "sequence"] } }, required: ["id", "mode", "type"], additionalProperties: false } }, { name: "get_sequence", description: "Retrieve detailed information about an integer sequence, including its terms, references, links, and keywords. The ID must match the sequence (e.g. A000045).", inputSchema: { type: "object", properties: { id: { type: "string", description: "ID of the integer sequence (e.g. A000045)" } }, required: ["id"], additionalProperties: false } }, { name: "search_sequences", description: "Search for integer sequences using flexible criteria. Supports pagination.\n" + "\nSupported search criteria:\n" + "- Name: Matches tokens in the sequence name (case-insensitive).\n" + "- ID: Matches tokens in the sequence ID (e.g., A000045).\n" + "- Keywords: Include keywords by specifying them in the query (e.g., 'core easy'). Exclude keywords by prefixing with a minus sign (e.g., '-hard').\n" + "- Operation Types: Include operation types (opcodes) of the corresponding LODA program (e.g., `mov add`). Exclude operation types by prefixing with a minus sign (e.g., `-mul`).\n" + "- Author: Matches tokens in the author names (case-insensitive).\n" + "- Submitter: Matches tokens in the submitter names of the corresponding LODA programs (case-insensitive).\n" + "- Advanced: All tokens in the query must be present in either the sequence name, author name, or submitter name. Keywords are handled as described above.\n" + "\nExample queries:\n" + "- 'Fibonacci core' (sequences with 'Fibonacci' in the name and the 'core' keyword)\n" + "- 'A000045' (sequence with ID A000045)" + "- 'Alice' (sequences authored by Alice or with programs submitted by Alice)\n" + "- '-hard' (exclude sequences with the 'hard' keyword)\n", inputSchema: { type: "object", properties: { q: { type: "string", description: "Search query supporting keywords, properties, submitters, and advanced criteria. To require a keyword, include it; to exclude, prefix with '-' (e.g., -core)." }, limit: { type: "number", description: "Maximum number of results to return (pagination limit)", minimum: 1, maximum: 100 }, skip: { type: "number", description: "Number of items to skip before starting to collect the result set (pagination offset)", minimum: 0 }, shuffle: { type: "boolean", description: "If set to true, the search results will be shuffled randomly" } }, required: ["q"], additionalProperties: false } }, { name: "get_stats", description: "Returns stats of the LODA project. This includes the number of sequences, programs, and formulas in the database.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }, { name: "get_keywords", description: "Returns a list of all keywords with their descriptions.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }, { name: "get_submitters", description: "Returns a list of all submitters with their number of submitted programs.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }, { name: "get_usage_stats", description: "Returns a list of all programs and the number of other programs that use them (calls via seq).", inputSchema: { type: "object", properties: {}, additionalProperties: false } } ] as Tool[] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Always ensure args is an object const safeArgs = (args && typeof args === 'object') ? args : {}; try { switch (name) { case "get_program_details": return this.handleGetProgramDetails(safeArgs as { id: string }); case "search_programs": return this.handleSearchPrograms(safeArgs as { q: string; limit?: number; skip?: number; shuffle?: boolean }); case "eval_program": return this.handleEvalProgram(safeArgs as { code: string; t?: number; o?: number }); case "export_program": return this.handleExportProgram(safeArgs as { code: string; format?: string }); case "get_submissions": return this.handleGetSubmissions(safeArgs as { limit?: number; skip?: number; mode?: string; type?: string; submitter?: string }); case "submit": return this.handleSubmit(safeArgs as { id: string; submitter: string; content: string; mode: string; type: string }); case "get_sequence": return this.handleGetSequence(safeArgs as { id: string }); case "search_sequences": return this.handleSearchSequences(safeArgs as { q: string; limit?: number; skip?: number; shuffle?: boolean }); case "get_stats": return this.handleGetStats(); case "get_keywords": return this.handleGetKeywords(); case "get_submitters": return this.handleGetSubmitters(); case "get_usage_stats": return this.handleGetUsageStats(); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { let errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError(ErrorCode.InternalError, `Error executing tool ${name}: ${errorMessage}`); } }); } private async handleGetSequence(args: { id: string }) { const { id } = args; if (!/^A\d{6,}$/.test(id)) { throw new McpError(ErrorCode.InvalidParams, "id must be a string like A000045"); } const seq = await this.apiClient.getSequence(id); return { content: [ { type: "text", text: `Sequence ${seq.id}: ${seq.name}\n` + `First terms: ${seq.terms.slice(0, 20).join(', ')}${seq.terms.length > 20 ? '...' : ''}\n` + (seq.keywords ? `Keywords: ${seq.keywords.join(', ')}\n` : '') + (Array.isArray((seq as any).authors) && (seq as any).authors.length ? `Authors: ${(seq as any).authors.join(', ')}\n` : '') + (seq.oeisRef ? `OEIS: ${seq.oeisRef}\n` : '') } ] }; } private async handleSearchSequences(args: { q: string; limit?: number; skip?: number; shuffle?: boolean }) { const { q, limit, skip, shuffle } = args; if (!q || typeof q !== 'string') { throw new McpError(ErrorCode.InvalidParams, "q is required"); } const result = await this.apiClient.searchSequences(q, limit, skip, shuffle); return { content: [ { type: "text", text: result.results.length === 0 ? 'No sequences found.' : result.results.map((r: {id: string, name: string, keywords?: string[]}) => `${r.id}: ${r.name}` + (r.keywords && r.keywords.length ? ` [${r.keywords.join(', ')}]` : '') ).join('\n') + `\nTotal: ${result.total}` } ], ...result }; } private async handleGetProgramDetails(args: { id: string }) { const { id } = args; if (!/^A\d{6,}$/.test(id)) { throw new McpError(ErrorCode.InvalidParams, "id must be a string like A000045"); } const prog = await this.apiClient.getProgram(id); let text = `LODA Program ${prog.id}: ${prog.name}\n` + `Submitter: ${prog.submitter || 'unknown'}\n` + `Code:\n${prog.code}`; if (Array.isArray(prog.usages) && prog.usages.length > 0) { text += `\nUsages: ${prog.usages.join(', ')}`; } return { content: [ { type: "text", text } ], usages: prog.usages }; } private async handleSearchPrograms(args: { q: string; limit?: number; skip?: number; shuffle?: boolean }) { const { q, limit, skip, shuffle } = args; if (!q || typeof q !== 'string') { throw new McpError(ErrorCode.InvalidParams, "q is required"); } const result = await this.apiClient.searchPrograms(q, limit, skip, shuffle); return { content: [ { type: "text", text: result.results.length === 0 ? 'No programs found.' : result.results.map((r: {id: string, name: string, keywords?: string[]}) => `${r.id}: ${r.name}` + (r.keywords && r.keywords.length ? ` [${r.keywords.join(', ')}]` : '') ).join('\n') + `\nTotal: ${result.total}` } ], ...result }; } private async handleEvalProgram(args: { code: string; t?: number; o?: number }) { const { code, t, o } = args; if (!code || typeof code !== 'string') { throw new McpError(ErrorCode.InvalidParams, "code is required"); } const result = await this.apiClient.evalProgram(code, t, o); return { content: [ { type: "text", text: result.status === "success" ? `Result: ${result.terms.join(', ')}` : `Error: ${result.message}${result.terms && result.terms.length ? `\nPartial result: ${result.terms.join(', ')}` : ''}` } ], ...result }; } private async handleExportProgram(args: { code: string; format?: string }) { const { code, format } = args; if (!code || typeof code !== 'string') { throw new McpError(ErrorCode.InvalidParams, "code is required"); } const result = await this.apiClient.exportProgram(code, format); return { content: [ { type: "text", text: result.status === "success" ? `Export result (${format || 'formula'}):\n${result.output}` : `Error: ${result.message}` } ], ...result }; } private async handleGetSubmissions(args: { limit?: number; skip?: number; mode?: string; type?: string; submitter?: string }) { const { limit, skip, mode, type, submitter } = args; const result = await this.apiClient.getSubmissions(limit, skip, mode, type, submitter); return { content: [ { type: "text", text: result.results.length === 0 ? 'No submissions found.' : result.results.map((s: Submission) => { const submitterText = s.submitter ? ` by ${s.submitter}` : ''; const contentPreview = s.content ? `:\n${s.content.slice(0, 100)}${s.content.length > 100 ? '...' : ''}` : ''; return `${s.id} (${s.mode}/${s.type})${submitterText}${contentPreview}`; }).join('\n\n') + `\n\nTotal: ${result.total}, Session: ${new Date(result.session * 1000).toISOString()}` } ], ...result }; } private async handleSubmit(args: { id: string; submitter?: string; content?: string; mode: string; type: string }) { const { id, submitter, content, mode, type } = args; if (!/^[A-Z]\d{1,10}$/.test(id)) { throw new McpError(ErrorCode.InvalidParams, "id must be a string like A000045"); } if (!['add', 'update', 'remove'].includes(mode)) { throw new McpError(ErrorCode.InvalidParams, "mode must be 'add', 'update', or 'remove'"); } if (!['program', 'sequence'].includes(type)) { throw new McpError(ErrorCode.InvalidParams, "type must be 'program' or 'sequence'"); } // Content is required for add and update, but not for remove if (mode !== 'remove' && (!content || typeof content !== 'string')) { throw new McpError(ErrorCode.InvalidParams, "content is required for add and update operations"); } const submission: Submission = { id, mode: mode as "add" | "update" | "remove", type: type as "program" | "sequence" }; if (submitter !== undefined) submission.submitter = submitter; if (content !== undefined) submission.content = content; const result = await this.apiClient.createSubmission(submission); return { content: [ { type: "text", text: result.status === "success" ? `Submission successful for ${id} (${mode}/${type})` : `Error: ${result.message}` } ], ...result }; } private async handleGetStats() { const stats = await this.apiClient.getStats(); return { content: [ { type: "text", text: `Stats: Sequences=${stats.numSequences}, Programs=${stats.numPrograms}, Formulas=${stats.numFormulas}` } ] }; } private async handleGetKeywords() { const keywords = await this.apiClient.getKeywords(); return { content: [ { type: "text", text: keywords.length === 0 ? 'No keywords found.' : keywords.map(k => `${k.name}: ${k.description}\n Programs: ${k.numPrograms}, Sequences: ${k.numSequences}` ).join('\n') } ], keywords }; } private async handleGetSubmitters() { const submitters = await this.apiClient.getSubmitters(); return { content: [ { type: "text", text: submitters.map(s => `${s.name}: ${s.numPrograms} programs`).join('\n') } ] }; } private async handleGetUsageStats() { // API returns array of { id: string, numUsages: number } const usages = await this.apiClient.getUsageStats(); return { content: [ { type: "text", text: usages.length === 0 ? 'No program usages found.' : usages.map((u: {id: string, numUsages: number}) => `${u.id}: ${u.numUsages} usages`).join('\n') } ], usages }; } async runStdio(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("LODA MCP server v2.0.0 running on stdio"); } // HTTP/Express/Streamable MCP static async runHttp(serverInstance: LODAMCPServer, port: number): Promise<void> { const app = express(); app.use(express.json()); // Session management for MCP const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; app.post('/v2/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; let transport: StreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { transport = transports[sessionId]; } else { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sid) => { transports[sid] = transport; }, }); // Connect only once per transport await serverInstance.server.connect(transport); transport.onclose = () => { if (transport.sessionId) delete transports[transport.sessionId]; }; } await transport.handleRequest(req, res, req.body); }); // GET and DELETE for notifications/session termination const handleSessionRequest = async (req: express.Request, res: express.Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports[sessionId]; await transport.handleRequest(req, res); }; app.get('/v2/mcp', handleSessionRequest); app.delete('/v2/mcp', handleSessionRequest); app.listen(port, () => { console.error(`LODA MCP server v2.0.0 running on http://localhost:${port}/v2/mcp`); }).on('error', (err: any) => { if (err.code === 'EADDRINUSE') { console.error(`Error: Port ${port} is already in use or the server is already running.`); process.exit(1); } else { console.error('Server error:', err); process.exit(1); } }); } } // Error handling for unhandled rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); }); process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); process.exit(1); }); // Create and start the server // Parse port from command line args: --port=PORT or -p PORT function getPortArg(): number | undefined { const argv = process.argv.slice(2); let port: number | undefined; for (let i = 0; i < argv.length; ++i) { if (argv[i] === '--port' && argv[i + 1]) { port = parseInt(argv[i + 1], 10); break; } else if (argv[i].startsWith('--port=')) { port = parseInt(argv[i].split('=')[1], 10); break; } else if ((argv[i] === '-p' || argv[i] === '--p') && argv[i + 1]) { port = parseInt(argv[i + 1], 10); break; } } return port; } const port = getPortArg(); const server = new LODAMCPServer(); if (port !== undefined) { LODAMCPServer.runHttp(server, port).catch((error) => { console.error("Failed to run LODA MCP HTTP server:", error); process.exit(1); }); } else { server.runStdio().catch((error) => { console.error("Failed to run LODA MCP server:", error); process.exit(1); }); }

Implementation Reference

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/loda-lang/loda-mcp'

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