1
Contributing
Prophet731 edited this page 2026-03-16 00:33:56 -04:00

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_case with 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:

  1. Download the updated openapi.yaml from VirtFusion docs
  2. Run the drift checker:
    npm run check-endpoint-drift
    
  3. 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]
    
  4. Implement the new tools
  5. Update the manifest:
    npm run extract-endpoints > endpoint-manifest.json
    
  6. 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 .js extensions (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-check workflow warns on PRs that change src/ without bumping the version in package.json
  • Tags follow the v1.2.3 format

For more on the architecture, see Architecture. For the full list of tools, see Tool-Reference.