We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/homelab-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
# Handler Development Guide
## Overview
This guide documents the standard patterns for implementing handlers in synapse-mcp. All handlers follow a consistent architecture that ensures type safety, security, and maintainability.
## Separation Of Concerns
Keep handlers focused on orchestration, not deep business logic.
Current pattern in this codebase:
1. Validate and type input at the boundary (`validateInput`).
2. Initialize shared context (`initializeHandler`).
3. Route by `action`/`subaction`.
4. Delegate complex operation details to focused helpers when needed.
5. Format response (`formatResponse`).
Examples in production code:
- Shared handler utilities: `src/tools/handlers/base-handler.ts`
- Compose-specific delegated operations: `src/tools/handlers/compose-handlers.ts`
- Cross-cutting compose helper logic: `src/tools/handlers/compose-utils.ts`
Guideline:
- When a `switch` branch grows beyond simple orchestration, extract it into a
dedicated helper function/module with its own tests.
- Keep transport/schema concerns in handlers and reusable operation logic in
helper modules or services.
## Standard Handler Pattern
All handlers follow this structure:
```typescript
import {
validateInput,
initializeHandler,
formatResponse,
getHostConfigOrThrow
} from "./base-handler.js"
import { myHandlerSchema } from "../../schemas/my-schema.js"
export async function handleMyAction(
input: InputType,
container: ServiceContainer
): Promise<string> {
// 1. Validate input at boundary (throws on invalid)
const validated = validateInput(input, myHandlerSchema, "my-handler")
// 2. Initialize handler context
const { service, hosts, format } = await initializeHandler(validated, container, (c) =>
c.getSSHService()
)
// 3. Resolve target host
const hostConfig = getHostConfigOrThrow(hosts, validated.host)
// 4. Execute operation
const result = await doSomething(service, hostConfig)
// 5. Format response
return formatResponse(result, format, () => formatAsText(result))
}
```
## Key Utilities
### validateInput
Validates handler input against a Zod schema at the handler boundary.
```typescript
const validated = validateInput(input, schema, "handler-name")
```
- Throws with standardized error format on validation failure
- Returns fully typed data on success
- Should be called at the very start of the handler
### initializeHandler
Initializes the handler context with service access and host loading.
```typescript
const { service, hosts, format } = await initializeHandler(input, container, (c) =>
c.getDockerService()
)
```
### getHostConfigOrThrow
Resolves a host by name from the loaded hosts list.
```typescript
const hostConfig = getHostConfigOrThrow(hosts, hostName)
```
### formatResponse
Formats the response based on the requested format (markdown or JSON).
```typescript
return formatResponse(data, format, () => formatAsMarkdown(data))
```
## TDD Workflow for New Handlers
Follow strict Test-Driven Development:
### 1. RED - Write Failing Schema Tests
```typescript
describe("myHandlerSchema", () => {
it("should accept valid input", () => {
const input = { action: "myaction", host: "test" }
expect(() => myHandlerSchema.parse(input)).not.toThrow()
})
it("should reject invalid input", () => {
const input = { action: "myaction" } // missing host
expect(() => myHandlerSchema.parse(input)).toThrow()
})
})
```
Run tests: Verify they FAIL
### 2. GREEN - Implement Schema
```typescript
export const myHandlerSchema = z.object({
action: z.literal("myaction"),
host: z.string().min(1)
})
```
Run tests: Verify they PASS
### 3. RED - Write Failing Handler Tests
```typescript
describe("handleMyAction", () => {
it("should use standardized validation error format", async () => {
const input = { action: "myaction" } // invalid
await expect(handleMyAction(input as any, mockContainer)).rejects.toThrow(
"my-handler input validation failed"
)
})
})
```
Run tests: Verify they FAIL
### 4. GREEN - Implement Handler
Implement the handler following the standard pattern.
Run tests: Verify they PASS
### 5. REFACTOR
- Extract helpers if function exceeds 50 lines
- Remove any type assertions
- Ensure proper typing throughout
## Security Checklist
When implementing handlers, ensure:
- [ ] All user input validated via Zod schema at handler entry
- [ ] Shell arguments use execFile (not shell string interpolation)
- [ ] Path traversal prevented via schema validation
- [ ] Defense-in-depth: security functions (validateSSHArg, validateSystemdServiceName) applied after Zod validation
- [ ] Sensitive data masked in logs
- [ ] No `as Record<string, unknown>` or similar unsafe casts
## Error Handling
Handlers use custom error classes for context preservation (primarily in service layers):
- `SSHCommandError` - SSH command execution failures
- `ComposeOperationError` - Docker Compose operations
- `HostOperationError` - Host-level operations
Validation failures at handler boundaries are thrown by `validateInput` as `Error` with a
standardized message format:
`<handler-name> input validation failed: <field>: <issue>, ...`
All errors preserve cause chains for debugging:
```typescript
throw new SSHCommandError(message, hostName, command, exitCode, stderr, stdout, cause)
```
## File Organization
```
src/
├── schemas/
│ ├── flux/ # Flux handler schemas
│ │ ├── docker.ts
│ │ ├── compose.ts
│ │ ├── container.ts
│ │ └── host.ts
│ └── scout/ # Scout handler schemas
│ ├── logs.ts
│ ├── zfs.ts
│ └── simple.ts
└── tools/
└── handlers/
├── base-handler.ts # Shared utilities
├── docker.ts
├── compose.ts
├── container.ts
├── host.ts
├── scout-logs.ts
├── scout-zfs.ts
└── scout-simple.ts
```
## Examples
### Migrated Handlers (Reference Implementations)
- `scout-logs.ts` - Uses validateInput with scoutLogsSchema
- `scout-zfs.ts` - Uses validateInput with scoutZfsSchema
- `scout-simple.ts` - Uses validateInput with scoutSimpleUnionSchema
- `docker.ts` - Uses validateInput with dockerUnionSchema
- `compose.ts` - Uses validateInput with composeUnionSchema
- `container.ts` - Uses validateInput with containerUnionSchema
- `host.ts` - Uses validateInput with hostUnionSchema