Template
91b78f8da1
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>
200 lines
7.5 KiB
TypeScript
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);
|
|
});
|