Skip to main content

Architecture Overview

HandsAI is built as a high-performance bridge service that connects Model Context Protocol (MCP) clients to arbitrary REST APIs through a dynamic tool registry.

System Architecture Diagram

Core Components

1. HandsAI Bridge (Go)

Purpose: Protocol translation layer between MCP’s stdio-based JSON-RPC and HandsAI’s HTTP REST API. Key Responsibilities:
  • Accept MCP tools/list requests and forward to /mcp/tools/list
  • Accept MCP tools/call requests and forward to /mcp/tools/call
  • Translate responses back to MCP format over stdio
  • Handle connection lifecycle and error propagation
Source: github.com/Vrivaans/handsai-bridge
The bridge is stateless and can be restarted without affecting HandsAI’s state. Configuration is optional via config.json for custom base URLs.

2. Spring Boot API (Java 21)

Tech Stack:
  • Framework: Spring Boot 3.5.4 (Spring MVC)
  • Java Version: Java 21 LTS
  • Concurrency: Virtual Threads via Executors.newVirtualThreadPerTaskExecutor()
  • Build Tool: Maven 3.8+
  • Native Compilation: GraalVM Native Image support
Main Controllers:
Implements the Model Context Protocol for tool discovery and execution.Endpoints:
  • GET /mcp/tools/list - Returns all enabled, healthy tools in MCP format
  • POST /mcp/tools/call - Executes a tool by name with JSON-RPC 2.0 protocol
Location: org.dynamcorp.handsaiv2.controller.MCPControllerError Handling:
  • -32602: Invalid params (missing required fields)
  • -32603: Internal error (execution failure)
All responses follow JSON-RPC 2.0 specification.
CRUD operations for individual tools (used by the frontend UI).Endpoints:
  • GET /admin/tools/api - List all tools
  • GET /admin/tools/api/{id} - Get tool by ID
  • POST /admin/tools/api - Create new tool
  • PUT /admin/tools/api/{id} - Update tool
  • DELETE /admin/tools/api/{id} - Delete tool
Location: org.dynamcorp.handsaiv2.controller.AdminToolController
Batch import providers and tools from JSON.Endpoint: POST /api/import/providersBehavior:
  • Upsert by code field (creates if new, updates if exists)
  • Preserves existing API keys if import value is <YOUR_API_KEY> placeholder
  • Creates tools and parameters in a single transaction
Location: org.dynamcorp.handsaiv2.controller.ImportController
Export providers and tools to JSON format.Endpoint: GET /api/export/providers?ids=1,2,3Security: Automatically obfuscates API keys to <YOUR_API_KEY> in export.Location: org.dynamcorp.handsaiv2.controller.ExportController

3. SQLite Database

Configuration:
  • File: handsai.db (auto-created in project root)
  • Mode: WAL (Write-Ahead Logging) for high concurrency
  • Dialect: org.hibernate.community.dialect.SQLiteDialect
  • Connection Pool: HikariCP with virtual threads
Core Tables:
CREATE TABLE api_provider (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  code TEXT UNIQUE NOT NULL,
  base_url TEXT NOT NULL,
  authentication_type TEXT, -- API_KEY, BEARER_TOKEN, BASIC_AUTH
  api_key_location TEXT,    -- HEADER, QUERY_PARAMETER, IN_BODY
  api_key_name TEXT,
  api_key_value TEXT,       -- Encrypted via Jasypt
  is_dynamic_auth BOOLEAN DEFAULT FALSE,
  dynamic_auth_url TEXT,
  dynamic_auth_method TEXT, -- GET, POST
  dynamic_auth_payload TEXT,
  dynamic_auth_token_extraction_path TEXT,
  custom_headers_json TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
API keys are encrypted at rest using Jasypt with AES-256. The encryption key is generated on first run and stored in environment variables.

4. ToolCacheManager

Purpose: In-memory cache for tool definitions to eliminate database queries on every MCP discovery request. Implementation: org.dynamcorp.handsaiv2.service.ToolCacheManager Data Structure:
private final ConcurrentHashMap<String, ApiTool> toolCache;
Lifecycle:
1

Initialization (@PostConstruct)

On application startup, loads all enabled tools from database:
@PostConstruct
public void initCache() {
    List<ApiTool> activeTools = apiToolRepository.findAllEnabled();
    activeTools.forEach(tool -> toolCache.put(tool.getCode(), tool));
    log.info("Tool cache initialized with {} tools", activeTools.size());
}
2

Cache Lookup

Tools are retrieved by code (O(1) lookup):
public Optional<ApiTool> getCachedTool(String toolCode) {
    return Optional.ofNullable(toolCache.get(toolCode))
        .filter(tool -> tool.isEnabled() && tool.isHealthy());
}
3

Cache Invalidation

Updated when tools are created, modified, or deleted via admin API.Manual refresh available via native tool:
# Refresh cache without restart
curl -X POST http://localhost:8080/mcp/tools/call \
  -d '{"params":{"name":"handsai_refresh_tool_cache"}}'
Performance Impact:
  • Without Cache: 50-100ms per discovery (database query + JSON serialization)
  • With Cache: < 5ms per discovery (memory lookup only)

Data Flow

Tool Discovery Flow

Tool Execution Flow

The automatic retry on 401 errors is a key feature of DynamicTokenManager. It detects expired tokens and refreshes them transparently without failing the LLM request.

Authentication Strategies

HandsAI supports multiple authentication methods:
Locations:
  • HEADER: X-API-Key: your_key or custom header name
  • QUERY_PARAMETER: ?key=your_key appended to URL
  • IN_BODY: {"api_key": "your_key"} in JSON body
Example (WeatherAPI):
{
  "authenticationType": "API_KEY",
  "apiKeyLocation": "QUERY_PARAMETER",
  "apiKeyName": "key",
  "apiKeyValue": "abc123..."  // Encrypted in DB
}

Advanced Features

1. Body Payload Templates

For APIs requiring nested JSON structures, use bodyPayloadTemplate with parameter interpolation. Example (Jules API):
{
  "bodyPayloadTemplate": "{\"prompt\": \"{{prompt}}\", \"sourceContext\": {\"source\": \"{{source}}\", \"githubRepoContext\": {\"startingBranch\": \"{{startingBranch}}\"}}}",
  "parameters": [
    {"name": "prompt", "type": "STRING", "required": true},
    {"name": "source", "type": "STRING", "required": true},
    {"name": "startingBranch", "type": "STRING", "required": false}
  ]
}
Execution: LLM provides: {"prompt": "Fix bug", "source": "sources/github/...", "startingBranch": "main"} HandsAI interpolates to:
{
  "prompt": "Fix bug",
  "sourceContext": {
    "source": "sources/github/...",
    "githubRepoContext": {
      "startingBranch": "main"
    }
  }
}

2. Array Parameter Handling

LLMs can send arrays as native JSON or string-encoded JSON. HandsAI automatically deserializes both. Definition:
{
  "name": "platforms",
  "type": "ARRAY",
  "description": "Social media platforms (e.g., ['linkedin', 'twitter'])"
}
LLM Sends (Option 1 - Native):
{"platforms": ["linkedin", "twitter"]}
LLM Sends (Option 2 - String):
{"platforms": "[\"linkedin\", \"twitter\"]"}
HandsAI Deserializes To:
{"platforms": ["linkedin", "twitter"]}
Source: ToolExecutionService.resolveArrayParams() at line 385

3. Path Parameter Substitution

Dynamic URL building for RESTful endpoints. Definition:
{
  "endpointPath": "/repos/{owner}/{repo}/issues"
}
LLM Provides:
{"owner": "facebook", "repo": "react", "title": "Bug report"}
Final URL:
https://api.github.com/repos/facebook/react/issues
Body (remaining params):
{"title": "Bug report"}

4. Logging & Analytics

Batch Processing: Execution logs are queued in-memory and persisted in batches to reduce SQLite write contention. Logged Data:
  • Tool name and session ID
  • Request payload (obfuscated)
  • Response payload (obfuscated)
  • Execution time (ms)
  • Success/failure status
  • Error messages
Source: org.dynamcorp.handsaiv2.service.LogBatchProcessor Obfuscation: API keys and sensitive headers are redacted in logs via LogObfuscator.

Virtual Threads (Java 21)

HandsAI uses Virtual Threads for all async operations, providing:
  • High Concurrency: Handle thousands of tool executions simultaneously
  • Low Overhead: Virtual threads are lightweight (< 1KB stack)
  • Simplified Code: No need for reactive programming or callbacks
Configuration:
@Configuration
public class VirtualThreadConfig {
    @Bean
    @Primary
    public Executor taskExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}
Location: org.dynamcorp.handsaiv2.config.VirtualThreadConfig

GraalVM Native Image

Compilation:
./mvnw -Pnative native:compile
Benefits:
  • Startup: < 1.5 seconds (vs. 3-5s on JVM)
  • Memory: ~100MB RSS (vs. 300-500MB on JVM)
  • Ideal For: Local AI agents, serverless, edge deployments
Compatibility Requirements: All reflection, dynamic proxies, and resource loading must be registered in NativeHintsConfig. Example (Registering Jasypt for encryption):
@Configuration
public class NativeHintsConfig implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        hints.reflection().registerType(
            TypeReference.of("org.jasypt.encryption.pbe.StandardPBEStringEncryptor"),
            MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
            MemberCategory.INVOKE_DECLARED_METHODS
        );
    }
}
Location: org.dynamcorp.handsaiv2.config.NativeHintsConfig
Always test native builds after adding dependencies that use reflection (e.g., Jackson mixins, dynamic class loading).

Scalability & Performance

Tool Discovery

Cached: < 5msUncached: 50-100ms(Typical: 100% cache hit rate)

Tool Execution

Local: 10-50msRemote API: 100-500ms(Depends on external API latency)

Concurrency

Virtual Threads: 10,000+ concurrent requestsSQLite WAL: Handles high read/write concurrency

Memory

JVM Mode: 300-500MBNative Mode: ~100MB(Scales with cache size)

Security Considerations

  • All API keys encrypted at rest using Jasypt (AES-256)
  • Encryption key stored in environment variables
  • Keys decrypted in-memory only during execution
  • Logs automatically obfuscate sensitive values
  • Input validated against tool parameter schemas
  • Required parameters enforced
  • Type checking for STRING, NUMBER, BOOLEAN, ARRAY
  • Malformed JSON rejected with 400 errors
  • Not implemented in HandsAI (delegated to external APIs)
  • Logging tracks execution frequency per tool
  • Consider adding Spring rate limiting for production
  • Default: @CrossOrigin(origins = "*") for development
  • Production: Configure specific origins in application.properties

Next Steps

API Reference

Complete endpoint documentation with request/response examples

Quickstart

Get HandsAI running with your first tool in 5 minutes

Use Cases

Explore pre-built integrations:
  • Weather API
  • Email (Resend)
  • Social Media (Ayrshare)
  • GitHub automation
  • Jules Agent API

Contributing

Read AGENTS.md for architecture rules and GraalVM compatibility guidelines

Build docs developers (and LLMs) love