From 6a3b65af9e22b5ea1a933e915a54d11fd059d0e4 Mon Sep 17 00:00:00 2001 From: Prophet731 Date: Mon, 16 Mar 2026 00:33:56 -0400 Subject: [PATCH] Add "Contributing" --- Contributing.md | 191 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 Contributing.md diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 0000000..e12fef5 --- /dev/null +++ b/Contributing.md @@ -0,0 +1,191 @@ +# Contributing + +This guide covers how to add new tools, follow project conventions, and use the development tooling. + +## Prerequisites + +```bash +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: + +```typescript +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: + +```typescript +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 = { 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`: + +```typescript +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 + +```bash +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: + +```typescript +const body: Record = { requiredField }; +if (optionalField !== undefined) body.optionalField = optionalField; +``` + +Do not send `undefined` values in the request body. + +## Development Commands + +```bash +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: + ```bash + 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: + ```bash + 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](https://semver.org/) +- 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]]. + +--- \ No newline at end of file