# Komodo MCP Server - Architecture Documentation
## Table of Contents
1. [Overview](#overview)
2. [System Architecture](#system-architecture)
3. [Project Structure](#project-structure)
4. [Core Abstractions](#core-abstractions)
5. [Module Organization](#module-organization)
6. [Type System](#type-system)
7. [Shared Utilities](#shared-utilities)
8. [Testing Strategy](#testing-strategy)
9. [Build and Development](#build-and-development)
10. [Security Considerations](#security-considerations)
11. [Implementation Approach](#implementation-approach)
---
## Overview
The Komodo MCP Server is a TypeScript-based Model Context Protocol (MCP) implementation that exposes the Komodo API through standardized tools accessible to AI assistants like Claude.
### Design Principles
1. **Modularity**: Clear separation of concerns across 6 API domains
2. **Type Safety**: Full TypeScript coverage with strict mode enabled
3. **Security First**: HMAC-based authentication with signature verification
4. **Testability**: Comprehensive test coverage with isolated unit tests
5. **Maintainability**: Consistent patterns and clear abstractions
6. **Performance**: Efficient connection pooling and caching
7. **Reliability**: Retry logic, timeout handling, and graceful error recovery
### Key Metrics
- **Total Tools**: 60 MCP tools across 6 modules
- **API Coverage**: Complete Komodo API surface
- **Type Definitions**: 100+ TypeScript interfaces
- **Test Coverage Target**: >90%
---
## System Architecture
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ MCP Client (Claude) │
└─────────────────────┬───────────────────────────────────────┘
│ MCP Protocol (stdio/SSE)
┌─────────────────────▼───────────────────────────────────────┐
│ MCP Server (index.ts) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Tool Registry & Router │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌───────▼──────┐ ┌───▼────┐ ┌─────▼──────┐
│ Auth Manager │ │ Logger │ │ Validators │
└───────┬──────┘ └────────┘ └────────────┘
│
┌───────▼────────────────────────────────────────────────────┐
│ Komodo HTTP Client │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ HMAC Signing | Retry Logic | Connection Pooling │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────────┘
│ HTTPS + HMAC Auth
┌─────────────────────▼───────────────────────────────────────┐
│ Komodo API Server │
│ (auth, user, read, write, execute, terminal modules) │
└─────────────────────────────────────────────────────────────┘
```
### Component Interaction Flow
```
1. Claude requests tool execution → MCP Server
2. MCP Server validates input → Schema validation
3. Tool handler invoked → Module-specific tool
4. Auth Manager signs request → HMAC signature
5. HTTP Client executes → Komodo API
6. Response processed → Type conversion
7. Result returned → Claude via MCP
```
---
## Project Structure
```
komodo-mcp/
├── src/
│ ├── index.ts # MCP server entry point
│ ├── server.ts # Server initialization
│ ├── config.ts # Configuration management
│ │
│ ├── core/
│ │ ├── KomodoClient.ts # HTTP client abstraction
│ │ ├── AuthManager.ts # Authentication & signing
│ │ ├── ToolRegistry.ts # Tool registration system
│ │ └── BaseToolHandler.ts # Base class for tools
│ │
│ ├── tools/
│ │ ├── auth/ # Auth module tools (4)
│ │ │ ├── index.ts
│ │ │ ├── Login.ts
│ │ │ ├── Logout.ts
│ │ │ ├── RefreshToken.ts
│ │ │ └── ValidateSession.ts
│ │ │
│ │ ├── user/ # User module tools (6)
│ │ │ ├── index.ts
│ │ │ ├── GetCurrentUser.ts
│ │ │ ├── ListUsers.ts
│ │ │ ├── GetUser.ts
│ │ │ ├── CreateUser.ts
│ │ │ ├── UpdateUser.ts
│ │ │ └── DeleteUser.ts
│ │ │
│ │ ├── read/ # Read module tools (16)
│ │ │ ├── index.ts
│ │ │ ├── servers.ts # Server operations
│ │ │ ├── deployments.ts # Deployment operations
│ │ │ ├── stacks.ts # Stack operations
│ │ │ ├── builds.ts # Build operations
│ │ │ ├── repos.ts # Repository operations
│ │ │ ├── procedures.ts # Procedure operations
│ │ │ ├── actions.ts # Action operations
│ │ │ └── alerts.ts # Alert operations
│ │ │
│ │ ├── write/ # Write module tools (21)
│ │ │ ├── index.ts
│ │ │ ├── servers.ts # Server CRUD
│ │ │ ├── deployments.ts # Deployment CRUD
│ │ │ ├── stacks.ts # Stack CRUD
│ │ │ ├── builds.ts # Build CRUD
│ │ │ ├── repos.ts # Repository CRUD
│ │ │ ├── procedures.ts # Procedure CRUD
│ │ │ └── actions.ts # Action CRUD
│ │ │
│ │ ├── execute/ # Execute module tools (9)
│ │ │ ├── index.ts
│ │ │ ├── Deploy.ts
│ │ │ ├── Build.ts
│ │ │ ├── StartServer.ts
│ │ │ ├── StopServer.ts
│ │ │ ├── RestartServer.ts
│ │ │ ├── RunProcedure.ts
│ │ │ ├── TriggerAction.ts
│ │ │ ├── PullRepo.ts
│ │ │ └── CloneRepo.ts
│ │ │
│ │ └── terminal/ # Terminal module tools (4)
│ │ ├── index.ts
│ │ ├── Connect.ts
│ │ ├── Execute.ts
│ │ ├── GetOutput.ts
│ │ └── Disconnect.ts
│ │
│ ├── types/
│ │ ├── index.ts # Type exports
│ │ ├── config.ts # Configuration types
│ │ ├── api.ts # API request/response types
│ │ ├── auth.ts # Authentication types
│ │ ├── user.ts # User types
│ │ ├── server.ts # Server types
│ │ ├── deployment.ts # Deployment types
│ │ ├── stack.ts # Stack types
│ │ ├── build.ts # Build types
│ │ ├── repo.ts # Repository types
│ │ ├── procedure.ts # Procedure types
│ │ ├── action.ts # Action types
│ │ ├── alert.ts # Alert types
│ │ ├── terminal.ts # Terminal types
│ │ └── errors.ts # Error types
│ │
│ └── utils/
│ ├── index.ts # Utility exports
│ ├── hmac.ts # HMAC signing utilities
│ ├── logger.ts # Logging utility
│ ├── validators.ts # Input validation
│ ├── errors.ts # Error handling
│ ├── retry.ts # Retry logic
│ ├── http.ts # HTTP helpers
│ └── formatters.ts # Response formatting
│
├── tests/
│ ├── setup.ts # Test setup/teardown
│ ├── mocks/ # Mock data and responses
│ │ ├── api.ts # API response mocks
│ │ └── fixtures.ts # Test fixtures
│ │
│ ├── unit/
│ │ ├── core/ # Core class tests
│ │ │ ├── KomodoClient.test.ts
│ │ │ ├── AuthManager.test.ts
│ │ │ └── ToolRegistry.test.ts
│ │ │
│ │ ├── tools/ # Tool tests (60 files)
│ │ │ ├── auth/
│ │ │ ├── user/
│ │ │ ├── read/
│ │ │ ├── write/
│ │ │ ├── execute/
│ │ │ └── terminal/
│ │ │
│ │ └── utils/ # Utility tests
│ │ ├── hmac.test.ts
│ │ ├── validators.test.ts
│ │ └── retry.test.ts
│ │
│ └── integration/
│ ├── auth-flow.test.ts # End-to-end auth flow
│ ├── server-lifecycle.test.ts
│ └── error-handling.test.ts
│
├── docs/
│ ├── ARCHITECTURE.md # This file
│ ├── API_MAPPING.md # API to tool mapping
│ ├── ENVIRONMENT.md # Environment configuration
│ └── IMPLEMENTATION.md # Implementation guide
│
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── tsconfig.build.json # Build-specific TS config
├── .eslintrc.json # ESLint configuration
├── .prettierrc # Prettier configuration
├── jest.config.js # Jest test configuration
└── README.md # Project documentation
```
---
## Core Abstractions
### 1. KomodoClient
The central HTTP client managing all Komodo API communication.
```typescript
// src/core/KomodoClient.ts
import { AuthManager } from './AuthManager';
import { Logger } from '../utils/logger';
import { retryWithBackoff } from '../utils/retry';
import type {
KomodoConfig,
HttpMethod,
RequestOptions,
ApiResponse
} from '../types';
export class KomodoClient {
private config: KomodoConfig;
private authManager: AuthManager;
private logger: Logger;
private baseUrl: string;
constructor(config: KomodoConfig) {
this.config = config;
this.authManager = new AuthManager(config);
this.logger = new Logger(config.logLevel);
this.baseUrl = config.url.replace(/\/$/, ''); // Remove trailing slash
}
/**
* Execute authenticated HTTP request to Komodo API
*/
async request<T>(
method: HttpMethod,
endpoint: string,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseUrl}${endpoint}`;
const { body, params, headers = {} } = options;
// Sign request with HMAC
const signedHeaders = this.authManager.signRequest(method, endpoint, body);
// Build request
const requestOptions: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
...signedHeaders,
...headers,
},
body: body ? JSON.stringify(body) : undefined,
};
// Add query parameters
const urlWithParams = params
? `${url}?${new URLSearchParams(params).toString()}`
: url;
// Execute with retry logic
return retryWithBackoff(
async () => {
this.logger.debug(`${method} ${urlWithParams}`);
const response = await fetch(urlWithParams, requestOptions);
if (!response.ok) {
throw await this.handleError(response);
}
const data = await response.json();
return {
data,
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
};
},
{
maxRetries: this.config.retryCount,
delayMs: this.config.retryDelay,
shouldRetry: (error) => this.isRetryableError(error),
}
);
}
/**
* GET request
*/
async get<T>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> {
return this.request<T>('GET', endpoint, options);
}
/**
* POST request
*/
async post<T>(endpoint: string, body: unknown, options?: RequestOptions): Promise<ApiResponse<T>> {
return this.request<T>('POST', endpoint, { ...options, body });
}
/**
* PUT request
*/
async put<T>(endpoint: string, body: unknown, options?: RequestOptions): Promise<ApiResponse<T>> {
return this.request<T>('PUT', endpoint, { ...options, body });
}
/**
* DELETE request
*/
async delete<T>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> {
return this.request<T>('DELETE', endpoint, options);
}
/**
* Handle HTTP errors
*/
private async handleError(response: Response): Promise<Error> {
const body = await response.text();
let errorMessage: string;
try {
const json = JSON.parse(body);
errorMessage = json.message || json.error || 'Unknown error';
} catch {
errorMessage = body || response.statusText;
}
const error = new Error(`Komodo API Error (${response.status}): ${errorMessage}`);
(error as any).status = response.status;
(error as any).response = response;
return error;
}
/**
* Determine if error should trigger retry
*/
private isRetryableError(error: any): boolean {
// Retry on network errors and 5xx status codes
if (!error.status) return true; // Network error
return error.status >= 500 && error.status < 600;
}
}
```
### 2. AuthManager
Handles HMAC-based authentication and request signing.
```typescript
// src/core/AuthManager.ts
import { createHmac } from 'crypto';
import type { KomodoConfig, SignedHeaders } from '../types';
export class AuthManager {
private apiKey: string;
private apiSecret: string;
constructor(config: KomodoConfig) {
this.apiKey = config.apiKey;
this.apiSecret = config.apiSecret;
}
/**
* Sign HTTP request with HMAC-SHA256
*
* Creates signature from: HTTP_METHOD + ENDPOINT + BODY + TIMESTAMP + SECRET
*/
signRequest(
method: string,
endpoint: string,
body?: unknown
): SignedHeaders {
const timestamp = Date.now().toString();
const bodyString = body ? JSON.stringify(body) : '';
// Signature payload: METHOD + ENDPOINT + BODY + TIMESTAMP
const payload = `${method}${endpoint}${bodyString}${timestamp}`;
// Create HMAC signature
const signature = createHmac('sha256', this.apiSecret)
.update(payload)
.digest('hex');
return {
'X-Komodo-Api-Key': this.apiKey,
'X-Komodo-Timestamp': timestamp,
'X-Komodo-Signature': signature,
};
}
/**
* Validate signature (for testing)
*/
validateSignature(
method: string,
endpoint: string,
body: unknown,
timestamp: string,
signature: string
): boolean {
const bodyString = body ? JSON.stringify(body) : '';
const payload = `${method}${endpoint}${bodyString}${timestamp}`;
const expectedSignature = createHmac('sha256', this.apiSecret)
.update(payload)
.digest('hex');
return signature === expectedSignature;
}
}
```
### 3. ToolRegistry
Manages registration and routing of MCP tools.
```typescript
// src/core/ToolRegistry.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { BaseToolHandler } from './BaseToolHandler';
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
export class ToolRegistry {
private tools: Map<string, BaseToolHandler> = new Map();
private server: Server;
constructor(server: Server) {
this.server = server;
}
/**
* Register a tool handler
*/
register(handler: BaseToolHandler): void {
const toolName = handler.getToolName();
if (this.tools.has(toolName)) {
throw new Error(`Tool already registered: ${toolName}`);
}
this.tools.set(toolName, handler);
}
/**
* Register multiple tools from a module
*/
registerModule(handlers: BaseToolHandler[]): void {
handlers.forEach(handler => this.register(handler));
}
/**
* Get all registered tools for MCP ListTools
*/
getAllTools(): Tool[] {
return Array.from(this.tools.values()).map(handler =>
handler.getToolDefinition()
);
}
/**
* Execute a tool by name
*/
async executeTool(name: string, args: Record<string, unknown>): Promise<any> {
const handler = this.tools.get(name);
if (!handler) {
throw new Error(`Tool not found: ${name}`);
}
return handler.execute(args);
}
/**
* Get tool by name
*/
getTool(name: string): BaseToolHandler | undefined {
return this.tools.get(name);
}
/**
* Check if tool exists
*/
hasTool(name: string): boolean {
return this.tools.has(name);
}
}
```
### 4. BaseToolHandler
Abstract base class for all tool implementations.
```typescript
// src/core/BaseToolHandler.ts
import { KomodoClient } from './KomodoClient';
import { Logger } from '../utils/logger';
import { validateInput } from '../utils/validators';
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import type { ToolInputSchema } from '../types';
export abstract class BaseToolHandler {
protected client: KomodoClient;
protected logger: Logger;
constructor(client: KomodoClient, logger: Logger) {
this.client = client;
this.logger = logger;
}
/**
* Get the tool name (e.g., "komodo_auth_Login")
*/
abstract getToolName(): string;
/**
* Get the tool description
*/
abstract getDescription(): string;
/**
* Get the input schema for validation
*/
abstract getInputSchema(): ToolInputSchema;
/**
* Execute the tool with validated input
*/
protected abstract executeImpl(args: Record<string, unknown>): Promise<any>;
/**
* Get full tool definition for MCP
*/
getToolDefinition(): Tool {
return {
name: this.getToolName(),
description: this.getDescription(),
inputSchema: this.getInputSchema(),
};
}
/**
* Execute tool with validation and error handling
*/
async execute(args: Record<string, unknown>): Promise<any> {
const toolName = this.getToolName();
try {
// Validate input
const schema = this.getInputSchema();
validateInput(args, schema);
this.logger.info(`Executing tool: ${toolName}`);
// Execute implementation
const result = await this.executeImpl(args);
this.logger.info(`Tool ${toolName} completed successfully`);
return result;
} catch (error) {
this.logger.error(`Tool ${toolName} failed:`, error);
throw error;
}
}
}
```
---
## Module Organization
### Module Pattern
Each API module follows a consistent structure:
```
tools/{module}/
├── index.ts # Module exports
├── {Operation}.ts # Individual tool handlers
└── README.md # Module documentation (optional)
```
### Tool Implementation Pattern
```typescript
// Example: src/tools/auth/Login.ts
import { BaseToolHandler } from '../../core/BaseToolHandler';
import type { ToolInputSchema } from '../../types';
export class LoginTool extends BaseToolHandler {
getToolName(): string {
return 'komodo_auth_Login';
}
getDescription(): string {
return 'Authenticate with Komodo API using username and password';
}
getInputSchema(): ToolInputSchema {
return {
type: 'object',
properties: {
username: {
type: 'string',
description: 'Username for authentication',
},
password: {
type: 'string',
description: 'Password for authentication',
},
},
required: ['username', 'password'],
};
}
protected async executeImpl(args: Record<string, unknown>): Promise<any> {
const { username, password } = args as { username: string; password: string };
const response = await this.client.post('/auth/login', {
username,
password,
});
return {
success: true,
token: response.data.token,
expiresAt: response.data.expiresAt,
};
}
}
```
### Module Index Pattern
```typescript
// Example: src/tools/auth/index.ts
import { LoginTool } from './Login';
import { LogoutTool } from './Logout';
import { RefreshTokenTool } from './RefreshToken';
import { ValidateSessionTool } from './ValidateSession';
export const authTools = [
LoginTool,
LogoutTool,
RefreshTokenTool,
ValidateSessionTool,
];
export {
LoginTool,
LogoutTool,
RefreshTokenTool,
ValidateSessionTool,
};
```
---
## Type System
### Configuration Types
```typescript
// src/types/config.ts
export interface KomodoConfig {
url: string;
apiKey: string;
apiSecret: string;
timeout: number;
retryCount: number;
retryDelay: number;
logLevel: LogLevel;
sslVerify: boolean;
}
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export interface EnvConfig {
KOMODO_URL: string;
KOMODO_API_KEY: string;
KOMODO_API_SECRET: string;
KOMODO_TIMEOUT?: string;
KOMODO_RETRY_COUNT?: string;
KOMODO_RETRY_DELAY?: string;
KOMODO_LOG_LEVEL?: LogLevel;
KOMODO_SSL_VERIFY?: string;
}
```
### API Types
```typescript
// src/types/api.ts
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
export interface RequestOptions {
body?: unknown;
params?: Record<string, string>;
headers?: Record<string, string>;
}
export interface ApiResponse<T> {
data: T;
status: number;
headers: Record<string, string>;
}
export interface SignedHeaders {
'X-Komodo-Api-Key': string;
'X-Komodo-Timestamp': string;
'X-Komodo-Signature': string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
```
### Authentication Types
```typescript
// src/types/auth.ts
export interface LoginRequest {
username: string;
password: string;
}
export interface LoginResponse {
token: string;
refreshToken: string;
expiresAt: number;
user: UserProfile;
}
export interface RefreshTokenRequest {
refreshToken: string;
}
export interface RefreshTokenResponse {
token: string;
expiresAt: number;
}
export interface SessionValidation {
valid: boolean;
expiresAt: number;
user: UserProfile;
}
```
### Resource Types
```typescript
// src/types/server.ts
export interface Server {
id: string;
name: string;
host: string;
port: number;
status: ServerStatus;
tags: string[];
createdAt: string;
updatedAt: string;
}
export type ServerStatus =
| 'running'
| 'stopped'
| 'starting'
| 'stopping'
| 'error';
export interface CreateServerRequest {
name: string;
host: string;
port: number;
tags?: string[];
}
export interface UpdateServerRequest {
name?: string;
host?: string;
port?: number;
tags?: string[];
}
```
### Error Types
```typescript
// src/types/errors.ts
export class KomodoError extends Error {
constructor(
message: string,
public code: string,
public status?: number,
public details?: unknown
) {
super(message);
this.name = 'KomodoError';
}
}
export class AuthenticationError extends KomodoError {
constructor(message: string, details?: unknown) {
super(message, 'AUTH_ERROR', 401, details);
this.name = 'AuthenticationError';
}
}
export class ValidationError extends KomodoError {
constructor(message: string, details?: unknown) {
super(message, 'VALIDATION_ERROR', 400, details);
this.name = 'ValidationError';
}
}
export class NotFoundError extends KomodoError {
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
}
}
export class NetworkError extends KomodoError {
constructor(message: string, details?: unknown) {
super(message, 'NETWORK_ERROR', undefined, details);
this.name = 'NetworkError';
}
}
```
---
## Shared Utilities
### HMAC Signing
```typescript
// src/utils/hmac.ts
import { createHmac, timingSafeEqual } from 'crypto';
export interface HmacConfig {
algorithm: 'sha256' | 'sha512';
encoding: 'hex' | 'base64';
}
export function generateHmacSignature(
payload: string,
secret: string,
config: HmacConfig = { algorithm: 'sha256', encoding: 'hex' }
): string {
return createHmac(config.algorithm, secret)
.update(payload)
.digest(config.encoding);
}
export function verifyHmacSignature(
payload: string,
signature: string,
secret: string,
config: HmacConfig = { algorithm: 'sha256', encoding: 'hex' }
): boolean {
const expected = generateHmacSignature(payload, secret, config);
// Timing-safe comparison to prevent timing attacks
try {
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
} catch {
return false;
}
}
```
### Error Handling
```typescript
// src/utils/errors.ts
import {
KomodoError,
AuthenticationError,
ValidationError,
NotFoundError,
NetworkError
} from '../types/errors';
export function handleApiError(error: any): never {
// Network errors
if (!error.status) {
throw new NetworkError('Network request failed', error);
}
// Authentication errors
if (error.status === 401 || error.status === 403) {
throw new AuthenticationError(error.message, error);
}
// Not found errors
if (error.status === 404) {
throw new NotFoundError('Resource', 'unknown');
}
// Validation errors
if (error.status === 400 || error.status === 422) {
throw new ValidationError(error.message, error);
}
// Generic error
throw new KomodoError(
error.message || 'Unknown error',
'UNKNOWN_ERROR',
error.status,
error
);
}
export function formatErrorForMcp(error: Error): {
error: string;
code?: string;
details?: unknown
} {
if (error instanceof KomodoError) {
return {
error: error.message,
code: error.code,
details: error.details,
};
}
return {
error: error.message,
code: 'INTERNAL_ERROR',
};
}
```
### Logging
```typescript
// src/utils/logger.ts
import type { LogLevel } from '../types/config';
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
export class Logger {
private level: LogLevel;
constructor(level: LogLevel = 'info') {
this.level = level;
}
debug(message: string, ...args: any[]): void {
this.log('debug', message, ...args);
}
info(message: string, ...args: any[]): void {
this.log('info', message, ...args);
}
warn(message: string, ...args: any[]): void {
this.log('warn', message, ...args);
}
error(message: string, ...args: any[]): void {
this.log('error', message, ...args);
}
private log(level: LogLevel, message: string, ...args: any[]): void {
if (LOG_LEVELS[level] >= LOG_LEVELS[this.level]) {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
console.error(prefix, message, ...args);
}
}
setLevel(level: LogLevel): void {
this.level = level;
}
}
```
### Validators
```typescript
// src/utils/validators.ts
import { ValidationError } from '../types/errors';
import type { ToolInputSchema } from '../types';
export function validateInput(
input: Record<string, unknown>,
schema: ToolInputSchema
): void {
// Check required fields
if (schema.required) {
for (const field of schema.required) {
if (!(field in input)) {
throw new ValidationError(`Missing required field: ${field}`);
}
}
}
// Validate field types
if (schema.properties) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (key in input) {
validateField(key, input[key], propSchema);
}
}
}
}
function validateField(
name: string,
value: unknown,
schema: any
): void {
const actualType = typeof value;
const expectedType = schema.type;
if (expectedType === 'string' && actualType !== 'string') {
throw new ValidationError(`Field ${name} must be a string`);
}
if (expectedType === 'number' && actualType !== 'number') {
throw new ValidationError(`Field ${name} must be a number`);
}
if (expectedType === 'boolean' && actualType !== 'boolean') {
throw new ValidationError(`Field ${name} must be a boolean`);
}
if (expectedType === 'array' && !Array.isArray(value)) {
throw new ValidationError(`Field ${name} must be an array`);
}
if (expectedType === 'object' && (actualType !== 'object' || Array.isArray(value))) {
throw new ValidationError(`Field ${name} must be an object`);
}
}
export function validateUrl(url: string): void {
try {
new URL(url);
} catch {
throw new ValidationError(`Invalid URL: ${url}`);
}
}
export function validateUuid(id: string): void {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(id)) {
throw new ValidationError(`Invalid UUID: ${id}`);
}
}
```
### Retry Logic
```typescript
// src/utils/retry.ts
export interface RetryOptions {
maxRetries: number;
delayMs: number;
shouldRetry?: (error: any) => boolean;
backoffMultiplier?: number;
}
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
options: RetryOptions
): Promise<T> {
const {
maxRetries,
delayMs,
shouldRetry = () => true,
backoffMultiplier = 2,
} = options;
let lastError: any;
let currentDelay = delayMs;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Don't retry if we've exhausted attempts
if (attempt >= maxRetries) {
break;
}
// Don't retry if error is not retryable
if (!shouldRetry(error)) {
break;
}
// Wait before retrying
await sleep(currentDelay);
currentDelay *= backoffMultiplier;
}
}
throw lastError;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
```
---
## Testing Strategy
### Test Organization
```
tests/
├── setup.ts # Global test setup
├── mocks/ # Shared mocks
│ ├── api.ts # API response mocks
│ └── fixtures.ts # Test data fixtures
│
├── unit/ # Unit tests (isolated)
│ ├── core/ # Core class tests
│ ├── tools/ # Tool handler tests
│ └── utils/ # Utility function tests
│
└── integration/ # Integration tests (end-to-end)
├── auth-flow.test.ts
├── server-lifecycle.test.ts
└── error-handling.test.ts
```
### Unit Test Pattern
```typescript
// tests/unit/core/KomodoClient.test.ts
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { KomodoClient } from '../../../src/core/KomodoClient';
import type { KomodoConfig } from '../../../src/types';
describe('KomodoClient', () => {
let client: KomodoClient;
let mockFetch: jest.Mock;
beforeEach(() => {
mockFetch = jest.fn();
global.fetch = mockFetch;
const config: KomodoConfig = {
url: 'https://api.test.com',
apiKey: 'test-key',
apiSecret: 'test-secret',
timeout: 30000,
retryCount: 3,
retryDelay: 1000,
logLevel: 'error',
sslVerify: true,
};
client = new KomodoClient(config);
});
describe('get', () => {
it('should execute GET request with signed headers', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true }),
headers: new Headers(),
});
await client.get('/test');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.test.com/test',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'X-Komodo-Api-Key': 'test-key',
'X-Komodo-Timestamp': expect.any(String),
'X-Komodo-Signature': expect.any(String),
}),
})
);
});
it('should retry on 500 errors', async () => {
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 500,
text: async () => 'Server error',
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ success: true }),
headers: new Headers(),
});
const result = await client.get('/test');
expect(result.data).toEqual({ success: true });
expect(mockFetch).toHaveBeenCalledTimes(2);
});
it('should not retry on 400 errors', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 400,
text: async () => 'Bad request',
});
await expect(client.get('/test')).rejects.toThrow('400');
expect(mockFetch).toHaveBeenCalledTimes(1);
});
});
});
```
### Tool Test Pattern
```typescript
// tests/unit/tools/auth/Login.test.ts
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { LoginTool } from '../../../../src/tools/auth/Login';
import { KomodoClient } from '../../../../src/core/KomodoClient';
import { Logger } from '../../../../src/utils/logger';
import { ValidationError } from '../../../../src/types/errors';
describe('LoginTool', () => {
let tool: LoginTool;
let mockClient: jest.Mocked<KomodoClient>;
let logger: Logger;
beforeEach(() => {
mockClient = {
post: jest.fn(),
} as any;
logger = new Logger('error');
tool = new LoginTool(mockClient, logger);
});
it('should have correct tool name', () => {
expect(tool.getToolName()).toBe('komodo_auth_Login');
});
it('should validate required fields', async () => {
await expect(
tool.execute({ username: 'test' })
).rejects.toThrow(ValidationError);
});
it('should execute login successfully', async () => {
mockClient.post.mockResolvedValue({
data: {
token: 'abc123',
expiresAt: 1234567890,
},
status: 200,
headers: {},
});
const result = await tool.execute({
username: 'testuser',
password: 'testpass',
});
expect(result).toEqual({
success: true,
token: 'abc123',
expiresAt: 1234567890,
});
expect(mockClient.post).toHaveBeenCalledWith('/auth/login', {
username: 'testuser',
password: 'testpass',
});
});
});
```
### Integration Test Pattern
```typescript
// tests/integration/auth-flow.test.ts
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { createServer } from '../../src/server';
import { startMockApi } from '../mocks/api';
describe('Authentication Flow', () => {
let server: Server;
let mockApi: any;
beforeAll(async () => {
mockApi = await startMockApi();
server = createServer();
});
afterAll(async () => {
await mockApi.close();
});
it('should complete full auth flow', async () => {
// 1. Login
const loginResult = await server.executeTool('komodo_auth_Login', {
username: 'testuser',
password: 'testpass',
});
expect(loginResult.success).toBe(true);
expect(loginResult.token).toBeDefined();
// 2. Validate session
const validateResult = await server.executeTool('komodo_auth_ValidateSession', {});
expect(validateResult.valid).toBe(true);
// 3. Logout
const logoutResult = await server.executeTool('komodo_auth_Logout', {});
expect(logoutResult.success).toBe(true);
// 4. Validate should now fail
await expect(
server.executeTool('komodo_auth_ValidateSession', {})
).rejects.toThrow();
});
});
```
### Test Coverage Requirements
| Component | Target Coverage | Priority |
|-----------|----------------|----------|
| Core Classes | 95% | Critical |
| Tool Handlers | 90% | High |
| Utilities | 95% | High |
| Type Guards | 100% | Medium |
| Error Handling | 90% | High |
| Integration | 80% | Medium |
---
## Build and Development
### TypeScript Configuration
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"],
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
```
### Build Configuration
```json
// tsconfig.build.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": true,
"declarationMap": false,
"removeComments": true
},
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts"]
}
```
### Package.json Scripts
```json
{
"scripts": {
"build": "tsc -p tsconfig.build.json",
"dev": "tsc --watch",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src tests --ext .ts",
"lint:fix": "eslint src tests --ext .ts --fix",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist",
"prebuild": "npm run clean",
"prepublishOnly": "npm run build"
}
}
```
### Development Workflow
```bash
# 1. Install dependencies
npm install
# 2. Run tests in watch mode
npm run test:watch
# 3. Start TypeScript compiler in watch mode
npm run dev
# 4. Run linter
npm run lint
# 5. Format code
npm run format
# 6. Build for production
npm run build
```
---
## Security Considerations
### 1. Environment Variables
- Never commit `.env` files
- Use secrets management in production
- Validate all environment variables on startup
- Provide clear error messages for missing vars
### 2. HMAC Signing
- Use timing-safe comparison for signature verification
- Include timestamp to prevent replay attacks
- Validate timestamp is within acceptable window (e.g., 5 minutes)
- Rotate API secrets periodically
### 3. Input Validation
- Validate all user input before processing
- Sanitize strings to prevent injection
- Validate UUIDs and URLs with strict patterns
- Use TypeScript types for compile-time safety
### 4. Error Handling
- Never expose sensitive data in error messages
- Log errors securely (no secrets in logs)
- Return generic errors to clients
- Track error patterns for security monitoring
### 5. Network Security
- Enforce HTTPS in production
- Verify SSL certificates (configurable for dev)
- Implement request timeouts
- Rate limit API calls (future enhancement)
### 6. Authentication
- Secure token storage
- Implement token expiration
- Support token refresh
- Clear tokens on logout
---
## Implementation Approach
### Phase 1: Foundation (Week 1)
**Goal**: Establish core infrastructure
1. **Setup Project**
- Initialize TypeScript project
- Configure build tools (tsc, eslint, prettier)
- Setup testing framework (Jest)
- Create directory structure
2. **Core Abstractions**
- Implement `KomodoClient`
- Implement `AuthManager` with HMAC signing
- Implement `ToolRegistry`
- Implement `BaseToolHandler`
3. **Utilities**
- Logger implementation
- Error handling utilities
- Retry logic with exponential backoff
- Input validation framework
4. **Testing Foundation**
- Setup test infrastructure
- Create mock factories
- Implement test utilities
**Deliverables**:
- Working HTTP client with HMAC auth
- Tool registration system
- Complete test setup
- Initial documentation
### Phase 2: Auth & User Modules (Week 1-2)
**Goal**: Implement authentication and user management
1. **Auth Module** (4 tools)
- `komodo_auth_Login`
- `komodo_auth_Logout`
- `komodo_auth_RefreshToken`
- `komodo_auth_ValidateSession`
2. **User Module** (6 tools)
- `komodo_user_GetCurrentUser`
- `komodo_user_ListUsers`
- `komodo_user_GetUser`
- `komodo_user_CreateUser`
- `komodo_user_UpdateUser`
- `komodo_user_DeleteUser`
3. **Testing**
- Unit tests for all tools
- Integration test for auth flow
- Mock API server for testing
**Deliverables**:
- 10 working tools
- Full test coverage
- Auth flow documentation
### Phase 3: Read Module (Week 2)
**Goal**: Implement read-only data operations
1. **Server Operations** (2 tools)
- `komodo_read_ListServers`
- `komodo_read_GetServer`
2. **Deployment Operations** (2 tools)
- `komodo_read_ListDeployments`
- `komodo_read_GetDeployment`
3. **Stack Operations** (2 tools)
- `komodo_read_ListStacks`
- `komodo_read_GetStack`
4. **Build Operations** (2 tools)
- `komodo_read_ListBuilds`
- `komodo_read_GetBuild`
5. **Repository Operations** (2 tools)
- `komodo_read_ListRepos`
- `komodo_read_GetRepo`
6. **Procedure Operations** (2 tools)
- `komodo_read_ListProcedures`
- `komodo_read_GetProcedure`
7. **Action Operations** (2 tools)
- `komodo_read_ListActions`
- `komodo_read_GetAction`
8. **Alert Operations** (2 tools)
- `komodo_read_ListAlerts`
- `komodo_read_GetAlert`
**Deliverables**:
- 16 read tools
- Pagination support
- Filtering/sorting support
- Complete test coverage
### Phase 4: Write Module (Week 3)
**Goal**: Implement data modification operations
1. **Server CRUD** (3 tools)
- `komodo_write_CreateServer`
- `komodo_write_UpdateServer`
- `komodo_write_DeleteServer`
2. **Deployment CRUD** (3 tools)
- `komodo_write_CreateDeployment`
- `komodo_write_UpdateDeployment`
- `komodo_write_DeleteDeployment`
3. **Stack CRUD** (3 tools)
- `komodo_write_CreateStack`
- `komodo_write_UpdateStack`
- `komodo_write_DeleteStack`
4. **Build CRUD** (3 tools)
- `komodo_write_CreateBuild`
- `komodo_write_UpdateBuild`
- `komodo_write_DeleteBuild`
5. **Repository CRUD** (3 tools)
- `komodo_write_CreateRepo`
- `komodo_write_UpdateRepo`
- `komodo_write_DeleteRepo`
6. **Procedure CRUD** (3 tools)
- `komodo_write_CreateProcedure`
- `komodo_write_UpdateProcedure`
- `komodo_write_DeleteProcedure`
7. **Action CRUD** (3 tools)
- `komodo_write_CreateAction`
- `komodo_write_UpdateAction`
- `komodo_write_DeleteAction`
**Deliverables**:
- 21 write tools
- Input validation for all create/update operations
- Optimistic concurrency support
- Complete test coverage
### Phase 5: Execute & Terminal Modules (Week 3-4)
**Goal**: Implement action execution and terminal operations
1. **Execute Module** (9 tools)
- `komodo_execute_Deploy`
- `komodo_execute_Build`
- `komodo_execute_StartServer`
- `komodo_execute_StopServer`
- `komodo_execute_RestartServer`
- `komodo_execute_RunProcedure`
- `komodo_execute_TriggerAction`
- `komodo_execute_PullRepo`
- `komodo_execute_CloneRepo`
2. **Terminal Module** (4 tools)
- `komodo_terminal_Connect`
- `komodo_terminal_Execute`
- `komodo_terminal_GetOutput`
- `komodo_terminal_Disconnect`
**Deliverables**:
- 13 execution tools
- WebSocket support for terminal
- Async operation handling
- Complete test coverage
### Phase 6: Integration & Documentation (Week 4)
**Goal**: Finalize integration and documentation
1. **Integration Testing**
- End-to-end server lifecycle tests
- Deployment workflow tests
- Error handling scenarios
- Performance testing
2. **Documentation**
- API reference for all tools
- Usage examples
- Integration guide
- Troubleshooting guide
3. **Production Readiness**
- Error handling review
- Security audit
- Performance optimization
- Deployment guide
**Deliverables**:
- Complete test suite
- Full documentation
- Production-ready server
- Deployment artifacts
---
## Success Metrics
### Functionality
- ✅ All 60 tools implemented
- ✅ All API endpoints covered
- ✅ HMAC authentication working
- ✅ Error handling complete
### Quality
- ✅ >90% test coverage
- ✅ Zero critical security issues
- ✅ All types properly defined
- ✅ Linting passes with no errors
### Performance
- ✅ <100ms average tool execution (excluding API)
- ✅ <500ms cold start time
- ✅ Retry logic functional
- ✅ Connection pooling working
### Documentation
- ✅ Complete architecture docs
- ✅ API reference for all tools
- ✅ Integration examples
- ✅ Troubleshooting guide
---
## Future Enhancements
### Near-term (Post-MVP)
1. Rate limiting support
2. Caching layer for read operations
3. Batch operations support
4. WebSocket reconnection logic
5. Metrics and monitoring
### Long-term
1. GraphQL API support
2. Event streaming
3. Advanced filtering/search
4. Multi-region support
5. CLI tool for testing
---
## Appendix
### Technology Stack
| Component | Technology | Version |
|-----------|-----------|---------|
| Runtime | Node.js | 20+ |
| Language | TypeScript | 5.0+ |
| MCP SDK | @modelcontextprotocol/sdk | Latest |
| Testing | Jest | 29+ |
| Linting | ESLint | 8+ |
| Formatting | Prettier | 3+ |
| HTTP Client | Native fetch | - |
### Dependencies
```json
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/jest": "^29.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"jest": "^29.0.0",
"prettier": "^3.0.0",
"ts-jest": "^29.0.0",
"typescript": "^5.0.0"
}
}
```
### Reference Links
- [Komodo API Documentation](https://github.com/mbecker20/komodo)
- [MCP Specification](https://modelcontextprotocol.io)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
- [Jest Documentation](https://jestjs.io)
---
**Document Version**: 1.0.0
**Last Updated**: 2026-01-26
**Authors**: System Architecture Designer
**Status**: Draft for Review