#!/usr/bin/env node /* Copyright (C) 2026 Moko Consulting * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: {{PROJECT_NAME}}.Server * INGROUP: {{PROJECT_NAME}} * REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}} * PATH: /src/index.ts * VERSION: 01.00.00 * BRIEF: MCP server entry point — registers all {{DISPLAY_NAME}} API tools */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { loadConfig, getConnection } from './config.js'; import { ApiClient } from './client.js'; import type { ApiConfig, ApiResponse } from './types.js'; let config: ApiConfig; function clientFor(connection?: string): ApiClient { return new ApiClient(getConnection(config, connection)); } function formatResponse(res: ApiResponse): { content: Array<{ type: 'text'; text: string }> } { if (res.status >= 400) { return { content: [{ type: 'text' as const, text: `Error: HTTP ${res.status}: ${JSON.stringify(res.data, null, 2)}` }], }; } return { content: [{ type: 'text' as const, text: JSON.stringify(res.data, null, 2) }], }; } // ── Shared parameter definitions ──────────────────────────────────────── const ConnectionParam = { connection: z.string().optional().describe('Named connection from config (uses default if omitted)'), }; const PaginationParams = { limit: z.number().optional().describe('Max results'), page: z.number().optional().describe('Page number (0-based)'), }; function paginationQuery(params: { limit?: number; page?: number }): Record { const q: Record = {}; if (params.limit !== undefined) q['limit'] = String(params.limit); if (params.page !== undefined) q['page'] = String(params.page); return q; } // ── Server ────────────────────────────────────────────────────────────── const server = new McpServer({ name: '{{PROJECT_NAME}}', version: '1.0.0', }); // ════════════════════════════════════════════════════════════════════════ // ADD YOUR TOOLS BELOW // ════════════════════════════════════════════════════════════════════════ // // Follow this pattern for each tool: // // server.tool( // 'prefix_resource_action', // tool name (snake_case) // 'Human-readable description', // shown to the AI assistant // { // Zod schema for parameters // id: z.number().describe('Resource ID'), // ...ConnectionParam, // }, // async ({ id, connection }) => { // const client = clientFor(connection); // return formatResponse(await client.get(`/resources/${id}`)); // }, // ); // // Tips: // - Group tools by resource type with section comments // - Use consistent naming: prefix_resource_list, prefix_resource_get, // prefix_resource_create, prefix_resource_update, prefix_resource_delete // - Spread ...ConnectionParam into every tool's schema // - Spread ...PaginationParams into list tools // - Use paginationQuery() to build query params for list endpoints // // ════════════════════════════════════════════════════════════════════════ // ── Example: Resources ────────────────────────────────────────────────── server.tool( 'example_resources_list', 'List resources (EXAMPLE — replace with your API resources)', { search: z.string().optional().describe('Search query'), ...PaginationParams, ...ConnectionParam, }, async ({ search, limit, page, connection }) => { const client = clientFor(connection); const params: Record = { ...paginationQuery({ limit, page }) }; if (search) params['search'] = search; return formatResponse(await client.get('/resources', params)); }, ); server.tool( 'example_resource_get', 'Get a single resource by ID (EXAMPLE — replace with your API resources)', { id: z.number().describe('Resource ID'), ...ConnectionParam, }, async ({ id, connection }) => { const client = clientFor(connection); return formatResponse(await client.get(`/resources/${id}`)); }, ); server.tool( 'example_resource_create', 'Create a new resource (EXAMPLE — replace with your API resources)', { name: z.string().describe('Resource name'), description: z.string().optional().describe('Resource description'), ...ConnectionParam, }, async ({ name, description, connection }) => { const client = clientFor(connection); const body: Record = { name }; if (description) body.description = description; return formatResponse(await client.post('/resources', body)); }, ); // ── Generic API Call ──────────────────────────────────────────────────── server.tool( 'api_request', 'Make a raw API request to any endpoint', { method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'), endpoint: z.string().describe('API endpoint path (e.g. "/resources")'), body: z.record(z.string(), z.unknown()).optional().describe('Request body for POST/PUT/PATCH'), params: z.record(z.string(), z.string()).optional().describe('Query parameters'), ...ConnectionParam, }, async ({ method, endpoint, body, params, connection }) => { const client = clientFor(connection); switch (method) { case 'GET': return formatResponse(await client.get(endpoint, params)); case 'POST': return formatResponse(await client.post(endpoint, body)); case 'PUT': return formatResponse(await client.put(endpoint, body)); case 'PATCH': return formatResponse(await client.patch(endpoint, body)); case 'DELETE': return formatResponse(await client.delete(endpoint)); } }, ); // ── Connections Management ────────────────────────────────────────────── server.tool( 'list_connections', 'List configured API connections', {}, async () => { const lines = Object.entries(config.connections).map(([name, conn]) => { const is_default = name === config.defaultConnection ? ' (default)' : ''; return ` ${name}${is_default}: ${conn.baseUrl}`; }); return { content: [{ type: 'text' as const, text: `Configured connections:\n${lines.join('\n')}` }], }; }, ); // ── Start Server ──────────────────────────────────────────────────────── async function main(): Promise { config = await loadConfig(); const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((err) => { process.stderr.write(`Fatal: ${err}\n`); process.exit(1); });