Welcome Contributors
HandsAI is a bridging API that acts as a universal dynamic registry connecting Model Context Protocol (MCP) clients with external REST APIs. This guide outlines the architecture, conventions, and requirements for contributing code.
Code Style
Lombok Annotations
HandsAI extensively uses Lombok to reduce boilerplate code. Follow these patterns:
@Entity
@Table(name = "api_providers")
@Getter
@Setter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class ApiProvider extends BaseModel {
private String name;
private String baseUrl;
// ...
}
Key annotations:
@Getter / @Setter - Auto-generate getters and setters
@SuperBuilder - Enable builder pattern with inheritance
@NoArgsConstructor / @AllArgsConstructor - Generate constructors
@Slf4j - Inject SLF4J logger instance
Logging with SLF4J
Always use @Slf4j for logging. HandsAI is deeply instrumented to provide visibility into what AI agents are doing:
@Service
@Slf4j
public class ToolExecutionService {
public void executeRemoteTool(String toolName, Map<String, Object> arguments) {
log.info("Executing tool: {} with arguments: {}", toolName, arguments);
try {
// execution logic
log.debug("Tool execution completed successfully");
} catch (Exception e) {
log.error("Tool execution failed: {}", e.getMessage(), e);
}
}
}
Log important execution paths, especially in:
ToolExecutionService - Tool invocations
- MCP handlers - Protocol communication
- Authentication flows - Security events
Architecture Principles
DTO Separation from Entities
Never return JPA Entities directly from Controllers. Always use DTOs (Data Transfer Objects).
Entities are persistence models; DTOs are API contracts. This separation:
- Prevents lazy-loading issues
- Avoids exposing internal database structure
- Enables data transformation and security filtering
Example - Entity:
@Entity
@Table(name = "api_providers")
public class ApiProvider extends BaseModel {
private String apiKeyValue; // Encrypted, should not be exposed
@OneToMany(mappedBy = "provider")
private List<ApiTool> tools;
}
Example - DTO (using Java Records):
public record ApiProviderResponse(
Long id,
String name,
String baseUrl,
AuthenticationTypeEnum authenticationType,
Map<String, String> customHeaders
) {
public static ApiProviderResponse from(ApiProvider provider) {
// Map entity to DTO, obscure sensitive data
return new ApiProviderResponse(
provider.getId(),
provider.getName(),
provider.getBaseUrl(),
provider.getAuthenticationType(),
parseHeaders(provider.getCustomHeadersJson())
);
}
}
Project Structure
When adding new features, follow this organization:
src/main/java/org/dynamcorp/handsaiv2/
├── controller/ # REST endpoints
│ ├── ProviderController.java
│ ├── MCPController.java
│ └── AdminToolController.java
├── service/ # Business logic
│ ├── ApiProviderService.java
│ ├── ToolExecutionService.java
│ └── EncryptionService.java
├── repository/ # Data access layer
│ ├── ApiProviderRepository.java
│ └── ApiToolRepository.java
├── model/ # JPA entities
│ ├── ApiProvider.java
│ ├── ApiTool.java
│ └── ToolParameter.java
├── dto/ # Data transfer objects
│ ├── ApiProviderResponse.java
│ └── CreateApiToolRequest.java
└── config/ # Configuration classes
├── NativeHintsConfig.java
└── SecurityConfig.java
Where to add new features:
- Controllers - New REST endpoints or MCP protocol handlers
- Services - Business logic, external API calls, orchestration
- Repositories - Database queries (extend
JpaRepository)
- Models - New database entities (extend
BaseModel)
- DTOs - API request/response structures
Testing Requirements
Unit Tests
Test service logic in isolation:
@SpringBootTest
class ApiProviderServiceTest {
@Autowired
private ApiProviderService providerService;
@Test
void shouldCreateProviderWithValidData() {
// Arrange
CreateApiProviderRequest request = new CreateApiProviderRequest(
"test-provider", "https://api.example.com"
);
// Act
ApiProviderResponse response = providerService.createProvider(request);
// Assert
assertNotNull(response.id());
assertEquals("test-provider", response.name());
}
}
Integration Tests
Test full request/response cycles with @WebMvcTest or @SpringBootTest.
GraalVM Compatibility Requirements
HandsAI must remain compatible with GraalVM Native Image compilation. This is CRITICAL for maintaining sub-1.5 second startup times.
Reflection Registration
GraalVM’s “closed-world assumption” removes classes not explicitly referenced. If your code uses:
- Reflection
- Dynamic proxies
- Class.forName()
- Jackson deserialization of complex types
You MUST register these types in NativeHintsConfig.java:
@Configuration
@ImportRuntimeHints(NativeHintsConfig.HandsAiRuntimeHints.class)
public class NativeHintsConfig {
public static class HandsAiRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register for reflection
hints.reflection().registerTypeIfPresent(classLoader,
"com.example.YourDynamicClass",
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS);
}
}
}
Resource Registration
Dynamically loaded resources (JSON templates, configuration files) must be registered:
hints.resources().registerPattern("templates/*.json");
Testing Native Compilation
After architectural changes, verify native compilation:
./mvnw -Pnative native:compile
This process takes several minutes but ensures GraalVM compatibility.
Pull Request Process
- Fork and Branch - Create a feature branch from
main
- Write Tests - Include unit tests for new functionality
- Follow Code Style - Use Lombok annotations, separate DTOs from entities
- Document Changes - Update relevant documentation
- Test Native Build - Run
./mvnw -Pnative native:compile for major changes
- Submit PR - Describe the problem solved and approach taken
PR Checklist
Database Conventions
HandsAI uses SQLite with specific conventions:
- Schema Evolution - Hibernate
update mode (see application.properties)
- WAL Mode - Write-Ahead Logging enabled for concurrency
- Error Handling - Handle
SQLITE_BUSY errors gracefully
- Transactions - Use
@Transactional for multi-step operations
Example error handling:
try {
repository.save(entity);
} catch (DataIntegrityViolationException e) {
log.error("Database constraint violation: {}", e.getMessage());
throw new ApiException("Entity already exists", e);
}
MCP Protocol Compliance
If modifying MCP-related code:
- HandsAI serves JSON-RPC 2.0 over HTTP
- Use standard MCP error codes (see
McpError.java)
- Tools are resolved dynamically from the database
- Responses cached in
ToolCacheManager
Example MCP response structure:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "weather_get",
"description": "Get current weather",
"inputSchema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
]
}
}
Additional Resources
Keep the code clean, startup time low, and never break native image execution!