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