Table of Contents
- Contributing
- Prerequisites
- Adding a New Tool
- Step 1: Identify the API Endpoint
- Step 2: Find or Create the Module
- Step 3: Implement the Tool
- Step 4: Register the Module (new modules only)
- Step 5: Update the Endpoint Manifest
- Step 6: Update Documentation
- Conventions
- Development Commands
- Checking for API Drift
- Code Style
- Versioning
Contributing
This guide covers how to add new tools, follow project conventions, and use the development tooling.
Prerequisites
git clone https://git.ezscale.cloud/EZSCALE/virtfusion-mcp.git
cd virtfusion-mcp
npm install
Adding a New Tool
Step 1: Identify the API Endpoint
Look up the endpoint in openapi.yaml. Note the HTTP method, path, request body schema, query parameters, and which tag it belongs to.
Step 2: Find or Create the Module
Each API tag maps to a file in src/tools/. For example:
- Tag
Servers->src/tools/servers.ts - Tag
Servers/Power->src/tools/servers-power.ts - Tag
Self Service->src/tools/self-service.ts
If the tag is new, create a new module file.
Step 3: Implement the Tool
Follow the existing pattern. Here is a complete example for a GET endpoint:
server.tool(
'category_action_noun', // snake_case name
'Human-readable description', // shown to the AI
{
// Zod schema for parameters
resourceId: z.number().describe('The resource ID'),
includeDetails: z.boolean().optional().describe('Include extra details'),
},
async ({ resourceId, includeDetails }) => {
try {
const result = await client.get(`/resource/${resourceId}`, { includeDetails });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
For a POST endpoint with a request body:
server.tool(
'category_create_noun',
'Create a new resource',
{
name: z.string().describe('Resource name'),
type: z.string().describe('Resource type'),
size: z.number().optional().describe('Size in GB'),
},
async ({ name, type, size }) => {
try {
const body: Record<string, unknown> = { name, type };
if (size !== undefined) body.size = size;
const result = await client.post('/resource', body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
Step 4: Register the Module (new modules only)
If you created a new module file, register it in src/tools/index.ts:
import { registerNewCategoryTools } from './new-category.js';
export function registerAllTools(server: McpServer, client: VirtFusionClient): void {
// ... existing registrations ...
registerNewCategoryTools(server, client);
}
Step 5: Update the Endpoint Manifest
npm run extract-endpoints > endpoint-manifest.json
Step 6: Update Documentation
- Update the tool count in
README.md - Add the new tool to the README's tool reference table under the appropriate module section
Conventions
Tool Naming
- Use
snake_casewith the pattern{category}_{action}_{noun} - Examples:
servers_power_boot,ip_blocks_list,self_service_add_credit - Every tool gets a human-readable description as the second argument to
server.tool()
Parameter Schemas
- Use Zod for all parameter definitions
- Always include
.describe()for every parameter -- these descriptions are shown to the AI - Use
.optional()for non-required parameters - Use
z.enum()for parameters with a fixed set of values (e.g.,z.enum(['primary', 'secondary'])) - Use
z.array()for array parameters
Error Handling
- Every handler must wrap its logic in
try/catch - The catch block must return
formatErrorResponse(error)-- never throw from a handler - This produces
{ isError: true, content: [...] }which the MCP client understands as a tool failure
HTTP Methods
- GET: Use
client.get(path, query?)-- query params go as the second argument - POST: Use
client.post(path, body?, query?)-- body is second, query is third - PUT: Use
client.put(path, body?, query?)-- same as POST - DELETE: Use
client.delete(path, body?, query?)-- some VirtFusion DELETE endpoints accept a body
Optional Body Parameters
When a POST/PUT has optional body fields, conditionally add them:
const body: Record<string, unknown> = { requiredField };
if (optionalField !== undefined) body.optionalField = optionalField;
Do not send undefined values in the request body.
Development Commands
npm run build # Compile TypeScript to dist/
npm run dev # Run with tsx (development, hot-reload friendly)
npm run extract-endpoints # Parse openapi.yaml to stdout JSON
npm run check-endpoint-drift # Compare spec vs manifest
Checking for API Drift
When VirtFusion updates their API:
- Download the updated
openapi.yamlfrom VirtFusion docs - Run the drift checker:
npm run check-endpoint-drift - The output shows new and removed endpoints:
Endpoint drift detected! New endpoints (2): + POST /servers/{serverId}/snapshot — Create a snapshot [Servers] + DELETE /servers/{serverId}/snapshot/{snapshotId} — Delete a snapshot [Servers] - Implement the new tools
- Update the manifest:
npm run extract-endpoints > endpoint-manifest.json - Commit everything together
The endpoint-sync CI workflow also runs weekly and creates a Gitea issue if drift is detected.
Code Style
- TypeScript with strict mode
- ES Modules (
"type": "module"in package.json) - All imports use
.jsextensions (required for Node.js ESM) - No additional linting or formatting tools are configured -- follow the style of existing code
Versioning
- The project follows Semantic Versioning
- The
version-checkworkflow warns on PRs that changesrc/without bumping the version inpackage.json - Tags follow the
v1.2.3format
For more on the architecture, see Architecture. For the full list of tools, see Tool-Reference.