Template
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>
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
/* 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}}.Client
|
||||
* INGROUP: {{PROJECT_NAME}}
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}}
|
||||
* PATH: /src/client.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: HTTP client for {{DISPLAY_NAME}} API
|
||||
*/
|
||||
|
||||
import * as https from 'node:https';
|
||||
import * as http from 'node:http';
|
||||
import type { ApiConnection, ApiResponse } from './types.js';
|
||||
|
||||
// ── Customize these ─────────────────────────────────────────────────────
|
||||
// API path prefix appended to baseUrl (e.g. "/api/index.php", "/api/v1")
|
||||
const API_PREFIX = '/api';
|
||||
const TIMEOUT_MS = 30_000;
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export class ApiClient {
|
||||
private readonly base_url: string;
|
||||
private readonly headers: Record<string, string>;
|
||||
private readonly insecure: boolean;
|
||||
|
||||
constructor(conn: ApiConnection) {
|
||||
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
|
||||
|
||||
// ── Customize auth headers ──────────────────────────────────
|
||||
// Examples:
|
||||
// Bearer token: { 'Authorization': `Bearer ${conn.apiKey}` }
|
||||
// API key header: { 'DOLAPIKEY': conn.apiKey }
|
||||
// Basic auth: { 'Authorization': `Basic ${btoa(user + ':' + pass)}` }
|
||||
this.headers = {
|
||||
'Authorization': `Bearer ${conn.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
this.insecure = conn.insecure ?? false;
|
||||
}
|
||||
|
||||
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint, params), 'GET');
|
||||
}
|
||||
|
||||
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'POST', body);
|
||||
}
|
||||
|
||||
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'PUT', body);
|
||||
}
|
||||
|
||||
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'PATCH', body);
|
||||
}
|
||||
|
||||
async delete(endpoint: string): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'DELETE');
|
||||
}
|
||||
|
||||
private buildUrl(endpoint: string, params?: Record<string, string>): string {
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
const url = new URL(`${this.base_url}${path}`);
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parsed = new URL(url);
|
||||
const is_https = parsed.protocol === 'https:';
|
||||
const transport = is_https ? https : http;
|
||||
|
||||
const options: https.RequestOptions = {
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port || (is_https ? 443 : 80),
|
||||
path: parsed.pathname + parsed.search,
|
||||
method,
|
||||
headers: { ...this.headers },
|
||||
timeout: TIMEOUT_MS,
|
||||
};
|
||||
|
||||
if (this.insecure && is_https) {
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
||||
if (payload) {
|
||||
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
|
||||
}
|
||||
|
||||
const req = transport.request(options, (res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString('utf-8');
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
data = raw;
|
||||
}
|
||||
resolve({ status: res.statusCode ?? 0, data });
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => reject(err));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timed out'));
|
||||
});
|
||||
|
||||
if (payload) {
|
||||
req.write(payload);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* 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}}.Config
|
||||
* INGROUP: {{PROJECT_NAME}}
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}}
|
||||
* PATH: /src/config.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: Configuration loader for {{DISPLAY_NAME}} MCP connections
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type { ApiConfig, ApiConnection } from './types.js';
|
||||
|
||||
// ── Customize this ──────────────────────────────────────────────────────
|
||||
// Change the filename to match your project (e.g. ".dolibarr-api-mcp.json")
|
||||
const CONFIG_FILENAME = '.{{PROJECT_NAME}}.json';
|
||||
// Change the env var name to match your project
|
||||
const CONFIG_ENV_VAR = '{{ENV_PREFIX}}_CONFIG';
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function loadConfig(): Promise<ApiConfig> {
|
||||
const config_path = process.env[CONFIG_ENV_VAR]
|
||||
? resolve(process.env[CONFIG_ENV_VAR]!)
|
||||
: resolve(homedir(), CONFIG_FILENAME);
|
||||
|
||||
try {
|
||||
const raw = await readFile(config_path, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as Partial<ApiConfig>;
|
||||
|
||||
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
||||
throw new Error('No connections defined in config');
|
||||
}
|
||||
|
||||
return {
|
||||
connections: parsed.connections,
|
||||
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
`Failed to load config from ${config_path}: ${message}\n` +
|
||||
`Create ${config_path} — see config.example.json for format.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getConnection(config: ApiConfig, name?: string): ApiConnection {
|
||||
const key = name ?? config.defaultConnection;
|
||||
const conn = config.connections[key];
|
||||
if (!conn) {
|
||||
throw new Error(
|
||||
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
|
||||
);
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
#!/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);
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/* 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}}.Types
|
||||
* INGROUP: {{PROJECT_NAME}}
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}}
|
||||
* PATH: /src/types.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: TypeScript type definitions for {{DISPLAY_NAME}} MCP server
|
||||
*/
|
||||
|
||||
/**
|
||||
* Connection configuration for a single API instance.
|
||||
*
|
||||
* Rename and extend these fields to match your target API's auth mechanism:
|
||||
* - `apiKey` + DOLAPIKEY header (Dolibarr)
|
||||
* - `apiToken` + Bearer header (Joomla, GitHub)
|
||||
* - `username`/`password` (Basic auth)
|
||||
* - `oauth` fields (OAuth2 flows)
|
||||
*/
|
||||
export interface ApiConnection {
|
||||
/** Base URL of the API instance (no trailing slash) */
|
||||
baseUrl: string;
|
||||
/** API key or token for authentication */
|
||||
apiKey: string;
|
||||
/** Skip TLS certificate verification (self-signed certs) */
|
||||
insecure?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level configuration supporting multiple named connections.
|
||||
*/
|
||||
export interface ApiConfig {
|
||||
connections: Record<string, ApiConnection>;
|
||||
defaultConnection: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized API response returned by the HTTP client.
|
||||
*/
|
||||
export interface ApiResponse {
|
||||
status: number;
|
||||
data: unknown;
|
||||
}
|
||||
Reference in New Issue
Block a user