Add "Contributing"

2026-03-16 00:33:56 -04:00
parent 5c42dc64f3
commit 6a3b65af9e

191
Contributing.md Normal file

@@ -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<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`:
```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<string, unknown> = { 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]].
---