HandsAI provides a robust import/export system for managing tool configurations as JSON. This enables version control, environment migration, and sharing curated tool collections.
The export format mirrors the database structure with security masking applied:
[
{
"name" : "Resend API" ,
"code" : "resend" ,
"baseUrl" : "https://api.resend.com" ,
"authenticationType" : "BEARER_TOKEN" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"apiKeyValue" : "<YOUR_API_KEY>" ,
"customHeaders" : {
"Content-Type" : "application/json" ,
"User-Agent" : "HandsAI/1.0"
},
"tools" : [
{
"name" : "Enviar Email (Resend)" ,
"code" : "resend-send-email" ,
"description" : "Envía un correo electrónico usando la API de Resend." ,
"endpointPath" : "/emails" ,
"httpMethod" : "POST" ,
"parameters" : [
{
"name" : "from" ,
"type" : "STRING" ,
"description" : "Dirección de envío (ej. Acme <[email protected] >)" ,
"required" : true ,
"defaultValue" : ""
},
{
"name" : "to" ,
"type" : "STRING" ,
"description" : "Dirección del destinatario." ,
"required" : true ,
"defaultValue" : ""
}
]
}
]
}
]
Source: ExportService.java:46-79 and NUEVOS_HITOS.json:2-54
Export All Exportable Providers Export all providers marked with isExportable: true: curl http://localhost:8080/api/export/providers > handsai_backup.json
Response Headers: Content-Type: application/json
Content-Disposition: attachment; filename=handsai_tools_export.json
Source: ExportController.java:25-38What Gets Exported? Only providers where isExportable = true are included. This is controlled at both provider and tool levels. Source: ExportService.java:34-38Export by Provider IDs Export only specific providers: curl "http://localhost:8080/api/export/providers?ids=1,3,5" > selected_tools.json
This exports providers with IDs 1, 3, and 5 (if they’re marked exportable). Source: ExportController.java:26
Security: API Key Masking
HandsAI automatically masks sensitive credentials during export:
{
"apiKeyValue" : "<YOUR_API_KEY>"
}
This placeholder prevents accidental credential leaks when sharing configurations.
Source: ExportService.java:26 and ImportService.java:35
Never commit real API keys to version control! Always verify exported JSON contains <YOUR_API_KEY> before sharing.
Import Endpoint
curl -X POST http://localhost:8080/api/import/providers \
-H "Content-Type: application/json" \
-d @handsai_backup.json
Response:
{
"message" : "Import successful"
}
Source: ImportController.java:24-30
Upsert Behavior
Imports use upsert logic based on the code field:
If code exists: Updates the existing provider/tool
If code is new: Creates a new provider/tool
If code is blank: Generates a new UUID
Source: ImportService.java:56-68 and ImportService.java:114-126
Create New
Update Existing
Creating New Providers When importing a provider that doesn’t exist: {
"code" : "new-api-service" ,
"name" : "New API Service" ,
"baseUrl" : "https://api.newservice.com" ,
"apiKeyValue" : "<YOUR_API_KEY>"
}
HandsAI will:
Check if code: "new-api-service" exists
Not found → Create new provider
Set createdAt to current timestamp
Source: ImportService.java:59-63Updating Existing Providers When importing a provider with matching code: {
"code" : "github" ,
"name" : "GitHub REST API v2" ,
"baseUrl" : "https://api.github.com"
}
HandsAI will:
Find existing provider with code: "github"
Update all fields with new values
Preserve createdAt, update updatedAt
Source: ImportService.java:64-68
API Key Placeholder Handling
When importing, HandsAI skips updating API keys if they contain the placeholder:
if ( req . apiKeyValue () != null &&
! req . apiKeyValue (). isBlank () &&
! req . apiKeyValue (). equals ( "<YOUR_API_KEY>" )) {
provider . setApiKeyValue ( encryptionService . encrypt ( req . apiKeyValue ()));
}
This means:
Real API keys are encrypted and stored
Placeholder <YOUR_API_KEY> is ignored
Existing API keys are preserved during updates
Source: ImportService.java:93-95
Complete Working Example
Here’s a complete example from the HandsAI source code - an Ayrshare integration for posting to multiple social platforms:
[
{
"name" : "Ayrshare Social Media API" ,
"code" : "ayrshare" ,
"baseUrl" : "https://app.ayrshare.com/api" ,
"authenticationType" : "API_KEY" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"apiKeyValue" : "<YOUR_API_KEY>" ,
"customHeaders" : {
"Content-Type" : "application/json"
},
"tools" : [
{
"name" : "Post to Social Media" ,
"code" : "ayrshare-post" ,
"description" : "Publishes content to multiple social media platforms" ,
"endpointPath" : "/post" ,
"httpMethod" : "POST" ,
"parameters" : [
{
"name" : "post" ,
"type" : "STRING" ,
"description" : "The content to publish" ,
"required" : true ,
"defaultValue" : ""
},
{
"name" : "platforms" ,
"type" : "ARRAY" ,
"description" : "Target platforms: linkedin, twitter, facebook, instagram" ,
"required" : true ,
"defaultValue" : "[ \" linkedin \" ]"
},
{
"name" : "mediaUrls" ,
"type" : "ARRAY" ,
"description" : "Array of image/video URLs to attach" ,
"required" : false ,
"defaultValue" : ""
},
{
"name" : "scheduleDate" ,
"type" : "STRING" ,
"description" : "ISO 8601 date for scheduling (optional)" ,
"required" : false ,
"defaultValue" : ""
}
]
}
]
}
]
Importing This Configuration
Save to file:
curl https://raw.githubusercontent.com/your-repo/ayrshare.json > ayrshare.json
Update API key:
# Replace the placeholder with your real key
sed -i 's/<YOUR_API_KEY>/Bearer AYRSHARE_KEY_abc123/g' ayrshare.json
Import:
curl -X POST http://localhost:8080/api/import/providers \
-H "Content-Type: application/json" \
-d @ayrshare.json
Verify:
curl http://localhost:8080/admin/providers | jq '.[] | select(.code=="ayrshare")'
Advanced: Dynamic Authentication
Some APIs require fetching tokens via OAuth or login endpoints. Here’s a Bluesky example:
{
"name" : "Bluesky API" ,
"code" : "bluesky" ,
"baseUrl" : "https://bsky.social/xrpc" ,
"authenticationType" : "BEARER_TOKEN" ,
"apiKeyLocation" : "HEADER" ,
"apiKeyName" : "Authorization" ,
"apiKeyValue" : "" ,
"isDynamicAuth" : true ,
"dynamicAuthUrl" : "https://bsky.social/xrpc/com.atproto.server.createSession" ,
"dynamicAuthMethod" : "POST" ,
"dynamicAuthPayloadType" : "JSON" ,
"dynamicAuthPayloadLocation" : "BODY" ,
"dynamicAuthPayload" : "{ \" identifier \" : \" your_handle.bsky.social \" , \" password \" : \" your_app_password \" }" ,
"dynamicAuthTokenExtractionPath" : "accessJwt" ,
"dynamicAuthInvalidationKeywords" : "ExpiredToken,AuthenticationRequired" ,
"tools" : [ /*...*/ ]
}
HandsAI will:
POST credentials to dynamicAuthUrl
Extract token from response using JSONPath (accessJwt)
Auto-refresh when response contains invalidation keywords
Source: NUEVOS_HITOS.json:197-232 and DynamicTokenManager.java
Migration Workflow
Development → Production
Export from dev:
curl http://dev.handsai.local:8080/api/export/providers > prod_migration.json
Update API keys for production:
# Edit prod_migration.json and replace <YOUR_API_KEY> with production credentials
vim prod_migration.json
Import to production:
curl -X POST https://prod.handsai.com/api/import/providers \
-H "Content-Type: application/json" \
-d @prod_migration.json
Tool Cache Auto-Refresh After import, the tool cache automatically refreshes. MCP clients will see new tools within seconds. Source: ImportService.java:51
Validation and Error Handling
Import errors are logged but don’t stop the batch:
# Check logs after import
curl http://localhost:8080/admin/tools/api | jq 'length'
# Expected: Number should match imported tools count
Common errors:
Missing code : UUID auto-generated
Invalid httpMethod : Must be GET/POST/PUT/DELETE/PATCH
Invalid type : Must be STRING/NUMBER/BOOLEAN/ARRAY/OBJECT
Missing baseUrl : Import fails for that provider
Source: ImportService.java:38-53
Best Practices
Version Control Your Exports
Track tool configurations in Git: mkdir -p config/handsai-tools
curl http://localhost:8080/api/export/providers > config/handsai-tools/ $( date +%Y%m%d ) .json
git add config/handsai-tools/
git commit -m "Snapshot HandsAI tools"
Use Environment Variables for Secrets
Store API keys outside JSON: export GITHUB_TOKEN = "ghp_abc123"
jq --arg token " $GITHUB_TOKEN " '.[0].apiKeyValue = $token' import.json | \
curl -X POST http://localhost:8080/api/import/providers -d @-
Test Imports in Development First
Always validate imports don’t break existing tools: # Backup before import
curl http://localhost:8080/api/export/providers > pre_import_backup.json
# Import
curl -X POST http://localhost:8080/api/import/providers -d @new_tools.json
# Test MCP discovery
curl http://localhost:8080/api/mcp/tools/list
Set isExportable Strategically
Mark only stable, production-ready tools as exportable: {
"isExportable" : true // Production-ready
}
Experimental tools should be false to avoid accidental exports.
Next Steps
Managing Providers Configure authentication and base URLs
Parameter Types Understanding ARRAY and OBJECT parameters
Use Case Examples Real-world integration examples
API Reference Complete import/export endpoint docs