Add "Architecture"

2026-03-16 00:33:56 -04:00
parent 34a2e009c4
commit 5c42dc64f3

166
Architecture.md Normal file

@@ -0,0 +1,166 @@
# Architecture
This page describes the project structure, key patterns, and the request flow from MCP tool invocation through to the VirtFusion API.
## Project Structure
```
virtfusion-mcp/
src/
index.ts Entry point: env validation, McpServer + StdioServerTransport setup
client.ts VirtFusionClient: fetch wrapper with Bearer auth, query param filtering
errors.ts VirtFusionApiError class + formatErrorResponse() for MCP error results
types.ts Shared interfaces (PaginatedResponse, QueryParams)
tools/
index.ts Barrel: registerAllTools() wires all 17 modules to McpServer
general.ts 1 tool (test connection)
hypervisors.ts 2 tools
hypervisor-groups.ts 3 tools
servers.ts 18 tools (largest module)
servers-power.ts 4 tools
servers-network.ts 5 tools
servers-firewall.ts 4 tools
servers-traffic.ts 4 tools
ip-blocks.ts 3 tools
backups.ts 1 tool
dns.ts 1 tool
media.ts 2 tools
packages.ts 2 tools
queue.ts 1 tool
ssh-keys.ts 4 tools
users.ts 7 tools
self-service.ts 19 tools
scripts/
extract-endpoints.ts Parse openapi.yaml into JSON endpoint manifest
check-endpoint-drift.ts Compare spec vs manifest for drift detection
openapi.yaml VirtFusion Admin API OpenAPI specification
endpoint-manifest.json Snapshot of all known endpoints (84 total)
.gitea/workflows/ CI/CD workflow definitions
```
## Core Modules
### Entry Point (`src/index.ts`)
The entry point performs three tasks:
1. **Environment validation** -- Checks for `VIRTFUSION_API_URL` and `VIRTFUSION_API_TOKEN`, exiting with a descriptive error if either is missing.
2. **Initialization** -- Creates a `VirtFusionClient` instance and an `McpServer` instance, then calls `registerAllTools()` to wire up all 84 tools.
3. **Transport** -- Connects the MCP server to a `StdioServerTransport` for communication with the MCP client.
### HTTP Client (`src/client.ts`)
`VirtFusionClient` is a lightweight fetch wrapper that handles:
- **Bearer authentication** -- Automatically adds `Authorization: Bearer <token>` to every request.
- **URL construction** -- Combines the base URL with the path and query parameters. Trailing slashes on the base URL are stripped.
- **Query parameter filtering** -- Any query parameter with an `undefined` value is silently omitted.
- **Content-Type handling** -- Only sets `Content-Type: application/json` when a body is present.
- **204 responses** -- Returns `{ success: true }` for No Content responses.
- **Error propagation** -- Non-OK responses throw a `VirtFusionApiError` with the status code, status text, and parsed response body.
The client exposes four public methods: `get()`, `post()`, `put()`, and `delete()`. All share the signature `(path, body?, query?)`.
### Error Handling (`src/errors.ts`)
Two exports:
- **`VirtFusionApiError`** -- An `Error` subclass carrying `statusCode`, `statusText`, and `errorBody`. Thrown by the client on non-OK responses.
- **`formatErrorResponse()`** -- Converts any caught error into an MCP-compatible `CallToolResult` with `isError: true`. For `VirtFusionApiError` instances, it includes structured status and error detail information. For other errors, it includes the error message.
### Shared Types (`src/types.ts`)
- **`PaginatedResponse<T>`** -- Interface matching VirtFusion's paginated response format (current_page, data, links, total, etc.).
- **`QueryParams`** -- Type alias for `Record<string, string | number | boolean | undefined>`.
## Tool Module Pattern
Every tool module in `src/tools/` follows the same pattern:
1. **Export a single registration function** with the signature:
```typescript
export function registerXxxTools(server: McpServer, client: VirtFusionClient): void
```
2. **Register tools** by calling `server.tool()` with four arguments:
- Tool name (snake_case)
- Human-readable description
- Zod schema object for parameter validation
- Async handler function
3. **Handler structure** -- Every handler follows this pattern:
```typescript
async (params) => {
try {
// Build body/query from params
const result = await client.get|post|put|delete(path, body?, query?);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
```
4. **Optional parameters** -- When a tool has optional body parameters, the handler conditionally adds them to the body object only when defined.
## Tool Registration
The barrel module `src/tools/index.ts` imports all 17 registration functions and exposes a single `registerAllTools()` function. The entry point calls this once during startup to wire everything up.
## Request Flow
```
MCP Client (Claude, VS Code, etc.)
|
| MCP protocol (stdio)
v
McpServer (src/index.ts)
|
| Zod validates parameters
v
Tool Handler (src/tools/*.ts)
|
| Builds request body/query from validated params
v
VirtFusionClient (src/client.ts)
|
| fetch() with Bearer auth headers
v
VirtFusion REST API
|
| JSON response
v
VirtFusionClient
|
| Parses response; throws VirtFusionApiError on non-OK
v
Tool Handler
|
| Wraps result in MCP content format, or catches error
v
McpServer
|
| MCP protocol (stdio)
v
MCP Client
```
## CI/CD Workflows
The project uses Gitea Actions with four workflows:
| Workflow | File | Trigger | Purpose |
|---|---|---|---|
| CI | `ci.yaml` | Push, PR | Build and type-check |
| Endpoint Sync | `endpoint-sync.yaml` | Weekly, `openapi.yaml` changes | Detect API drift, create issue |
| Release | `release.yaml` | Semver tag push | Create Gitea release with changelog |
| Version Check | `version-check.yaml` | PRs changing `src/` | Warn if `package.json` version not bumped |
## Key Design Decisions
- **No runtime dependencies beyond MCP SDK and Zod** -- The client uses Node.js built-in `fetch` (available in Node 22+).
- **Flat tool namespace** -- All 84 tools share a single namespace with `snake_case` naming convention `{category}_{action}_{noun}`.
- **Server-side safety** -- The `servers_delete` tool enforces a minimum 300-second delay to prevent accidental destruction.
- **All responses as JSON text** -- Every tool returns its API response as pretty-printed JSON in a text content block, keeping the interface uniform.
---