#!/usr/bin/env node /** * MCP server for the landing API. * * Reads the OpenAPI document the running application serves and offers one * tool per documented operation, so it always describes the API it is talking * to — local, staging or production — rather than the one in this checkout. * * node server.mjs stdio, for a client that launches it locally * node server.mjs --http Streamable HTTP on /mcp, for deploying * * Configuration, all through the environment: * * LANDING_URL Application root. Default http://localhost:8000 * LANDING_OPENAPI Path or URL of the spec, if not LANDING_URL/openapi.yaml. * LANDING_PREVIEW_TOKEN Lets the public tools see unpublished content. Over * HTTP, only for signed-in callers. * * stdio only — the one person running it is the one it signs in as: * * LANDING_USERNAME Pireon credentials for the admin API (/v1). The * LANDING_PASSWORD token is obtained and refreshed automatically. * LANDING_TOKEN Or a fixed bearer token instead. * * HTTP only — every request brings its own credentials (see src/http.mjs): * * LANDING_MCP_PORT Default 3000. * LANDING_MCP_HOST Default 127.0.0.1; the image sets 0.0.0.0. * LANDING_MCP_REQUIRE_AUTH "true" refuses anonymous callers instead of * offering them the public API. * * stdout is the stdio protocol channel: anything for a human goes to stderr. */ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { Catalogue } from './src/catalogue.mjs'; import { LandingClient } from './src/client.mjs'; import { startHttpServer } from './src/http.mjs'; import { createMcpServer } from './src/mcp.mjs'; const env = (name) => process.env[name]?.trim() || undefined; const baseUrl = (env('LANDING_URL') ?? 'http://localhost:8000').replace(/\/+$/, ''); const http = process.argv.includes('--http'); const catalogue = new Catalogue({ source: env('LANDING_OPENAPI') ?? `${baseUrl}/openapi.yaml`, baseUrl, // A deployment outlives many deploys of the application; a stdio process // lives for one editor session and describes its tools once anyway. ttlMs: http ? 60_000 : Infinity, }); if (http) { // Credentials in the environment would sign every anonymous caller in as // whoever they belong to. for (const name of ['LANDING_USERNAME', 'LANDING_PASSWORD', 'LANDING_TOKEN']) { if (env(name)) console.error(`landing-mcp: ${name} is ignored over HTTP; each request brings its own credentials`); } // Tried now only to report a misconfiguration early. The application may // well still be starting, so a failure here is not fatal: the catalogue // retries on the first request. catalogue.get().catch((error) => console.error(`landing-mcp: OpenAPI document not available yet: ${error.message}`)); startHttpServer({ catalogue, port: Number(env('LANDING_MCP_PORT') ?? 3000), host: env('LANDING_MCP_HOST') ?? '127.0.0.1', previewToken: env('LANDING_PREVIEW_TOKEN'), requireAuth: env('LANDING_MCP_REQUIRE_AUTH') === 'true', }); } else { let current; try { current = await catalogue.get(); } catch (error) { console.error(`landing-mcp: could not load the OpenAPI document from ${catalogue.source}: ${error.message}`); process.exit(1); } const client = new LandingClient({ apiBase: current.apiBase, token: env('LANDING_TOKEN'), username: env('LANDING_USERNAME'), password: env('LANDING_PASSWORD'), previewToken: env('LANDING_PREVIEW_TOKEN'), }); await createMcpServer({ spec: current.spec, tools: current.tools, client }).connect(new StdioServerTransport()); console.error(`landing-mcp: stdio against ${current.apiBase}${client.canAuthenticate() ? '' : ' (public API only: no credentials)'}`); }