Files
Template-NPM/src/index.ts
T
Jonathan Miller 91b78f8da1 feat: initial MCP server template with placeholder-driven scaffolding
Template repository for creating MokoStandards-compliant MCP servers.
Includes 4-file src/ structure (index, client, config, types), setup
wizard, example tools, 12 CI/CD workflows, full docs, and {{placeholder}}
tokens for search-and-replace customization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 14:39:19 -05:00

200 lines
7.5 KiB
TypeScript

#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* 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<string, string> {
const q: Record<string, string> = {};
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<string, string> = { ...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<string, unknown> = { 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<void> {
config = await loadConfig();
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
process.stderr.write(`Fatal: ${err}\n`);
process.exit(1);
});