Add "Contributing"
191
Contributing.md
Normal file
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]].
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user