API Reference
Complete guide to the Hay plugin API
Hay Plugin API Documentation#
Complete guide to building, extending, and working with the Hay plugin system
Table of Contents#
- Overview
- Architecture
- Plugin Manifest Reference
- Plugin Types
- MCP Integration
- Plugin Lifecycle
- API Reference
- Configuration Management
- UI Extensions
- Internationalization (i18n)
- Channel Registration
- Best Practices
- Building New Features
- Troubleshooting
Overview#
The Hay plugin system is a dynamic, modular architecture that allows extending platform functionality through plugins. Plugins can:
- Connect to external services via MCP (Model Context Protocol)
- Provide AI tools and capabilities
- Register communication channels
- Extend the UI with custom components
- Add API routes and backend functionality
- Implement OAuth authentication flows
Core Principles#
- Dynamic Loading: Plugins are discovered and loaded at runtime
- Organization Isolation: Custom plugins are scoped to organizations
- On-Demand Activation: Plugin instances start only when needed
- MCP-First: Built around the Model Context Protocol standard
- Never Hardcode: Core code should never reference specific plugin IDs
Architecture#
Directory Structure#
plugins/
└── core/ # Core plugins (all orgs)
├── atlassian/
├── calcom/
├── chatwoot/
├── email/
├── hubspot/
├── klaviyo/
├── magento/
├── notion/
├── stripe/
├── twenty/
├── whatsapp/
├── woocommerce/
├── zendesk/
└── ...
Note: Plugin discovery now uses
package.jsonwith ahay-pluginblock rather than a separateplugins/base/plugin-manifest.schema.jsonschema file.
Plugin Structure#
Each plugin directory contains:
{plugin-name}/
├── manifest.json # Plugin configuration (REQUIRED)
├── package.json # NPM dependencies
├── tsconfig.json # TypeScript configuration
├── src/ # TypeScript source code
│ └── index.ts # Plugin entry point
├── dist/ # Compiled output
│ └── index.js # Compiled entry (specified in manifest)
├── mcp/ # MCP server code (if local)
│ ├── index.js # MCP server entry
│ └── package.json # MCP dependencies
├── i18n/ # Translations (optional)
│ ├── en.json # English (fallback)
│ └── pt-BR.json # Brazilian Portuguese
├── components/ # Vue components (UI extensions)
│ └── settings/
│ └── CustomSettings.vue
└── public/ # Static assets
└── icon.png
System Components#
1. Plugin Manager Service#
Location: server/services/plugin-manager.service.ts
Responsible for:
- Plugin discovery and registration
- Manifest validation
- Installation and building
- Checksum calculation
- Plugin registry management
2. Plugin Instance Manager Service#
Location: server/services/plugin-instance-manager.service.ts
Handles:
- On-demand instance startup
- Instance lifecycle management
- Inactivity cleanup (5-minute timeout)
- Pool limits and queueing
- Activity tracking
3. Plugin Runner Service#
Location: server/services/plugin-runner.service.ts
Manages:
- MCP server process lifecycle
- Process health monitoring
- Environment variable injection
- Communication via stdio/SSE/WebSocket
4. MCP Client Factory#
Location: server/services/mcp-client-factory.service.ts
Provides:
- MCP client creation for local/remote servers
- Connection management
- Tool invocation
- Transport protocol handling
Plugin Manifest Reference#
The manifest.json file is the heart of every plugin. Plugin discovery uses package.json with a hay-plugin block to register and configure each plugin.
Required Fields#
{
"id": "hay-plugin-{name}", // Unique identifier (alphanumeric + hyphens)
"name": "Plugin Display Name", // Human-readable name
"version": "1.0.0", // Semantic version
"description": "Plugin description", // Brief description
"author": "Author Name", // Author or organization
"type": ["mcp-connector"], // Plugin type(s) - see Plugin Types
"entry": "./dist/index.js" // Compiled entry point
}
Optional Core Fields#
{
"enabled": true, // Default enabled state
"category": "integration", // Marketplace category
"icon": "shopify", // Icon identifier or URL
"autoActivate": false, // Auto-start on server boot
"invisible": false, // Hide from marketplace
"trpcRouter": "./dist/router.js" // Custom tRPC router (with autoActivate)
}
Marketplace Configuration#
{
"marketplace": {
"featured": true, // Feature in marketplace
"tags": ["ecommerce", "payments"], // Searchable tags
"screenshots": [
// Screenshot URLs
"https://example.com/screenshot1.png"
]
}
}
Capabilities#
The capabilities object defines what the plugin can do:
MCP Capabilities#
{
"capabilities": {
"mcp": {
"connection": {
"type": "local", // "local" or "remote"
"url": "https://mcp.example.com" // Required if type=remote
},
"tools": [
// MCP tools provided
{
"name": "tool_name",
"label": "Tool Display Name",
"description": "What this tool does",
"input_schema": {
// JSON Schema for parameters
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "Parameter description"
}
},
"required": ["param1"]
}
}
],
"transport": "sse|websocket|http", // Transport protocol(s)
"auth": ["apiKey"], // Auth methods - see Authentication
"installCommand": "npm install", // Install dependencies
"buildCommand": "npm run build", // Build command (optional)
"startCommand": "node mcp/index.js" // Start MCP server
}
}
}
UI Capabilities#
{
"capabilities": {
"ui": {
"routes": [
// Custom routes
{
"path": "/plugins/myplugin",
"component": "./components/Page.vue",
"name": "my-plugin-page",
"meta": {
"title": "My Plugin",
"requiresAuth": true
}
}
],
"components": [
// Reusable components
{
"name": "MyComponent",
"path": "./components/MyComponent.vue"
}
]
}
}
}
API Capabilities#
{
"capabilities": {
"api": {
"routes": [
// Custom API routes
{
"method": "POST",
"path": "/webhook",
"handler": "./handlers/webhook.js",
"middleware": ["auth"]
}
]
}
}
}
Authentication#
Simple Auth Methods#
{
"capabilities": {
"mcp": {
"auth": ["apiKey"] // or ["oauth2"], ["jwt"], or []
}
}
}
OAuth 2.0 Configuration#
{
"capabilities": {
"mcp": {
"auth": {
"methods": ["oauth2", "apiKey"],
"oauth": {
"authorizationUrl": "https://provider.com/oauth/authorize",
"tokenUrl": "https://provider.com/oauth/token",
"scopes": ["read", "write"], // Required scopes
"optionalScopes": ["admin"], // Optional scopes
"pkce": true, // Enable PKCE
"clientIdEnvVar": "PLUGIN_CLIENT_ID",
"clientSecretEnvVar": "PLUGIN_CLIENT_SECRET"
}
}
}
}
}
Configuration Schema#
Define user-configurable fields:
{
"configSchema": {
"apiKey": {
"type": "string",
"description": "Your API key for authentication",
"label": "API Key",
"placeholder": "sk_live_...",
"required": true,
"encrypted": true, // Store encrypted in database
"env": "PLUGIN_API_KEY", // Environment variable name
"regex": "^sk_(test|live)_.*" // Validation regex (optional)
},
"webhookUrl": {
"type": "string",
"description": "Webhook endpoint URL",
"label": "Webhook URL",
"required": false,
"default": "https://example.com/webhook"
}
}
}
Supported field types: string, number, boolean, array, object
Permissions#
{
"permissions": {
"env": [
// Required environment variables
"SHOPIFY_ACCESS_TOKEN",
"MYSHOPIFY_DOMAIN"
],
"scopes": [
// Required permission scopes
"org:<organizationId>:mcp:invoke"
],
"api": [
// Platform APIs (Plugin API pattern)
"email" // Only declared APIs can be accessed
]
}
}
UI Extensions#
Settings Extensions#
Add UI to plugin settings pages:
{
"settingsExtensions": [
{
"slot": "before-settings", // "before-settings", "after-settings", or "tab"
"component": "components/settings/CustomSection.vue",
"tabName": "Advanced", // Required if slot="tab"
"tabOrder": 1 // Order of tab (optional)
}
]
}
Plugin Types#
Plugins can have multiple types for categorization:
Available Types#
| Type | Description | Use Case |
|---|---|---|
channel |
Communication channel integration | WhatsApp, Telegram, Email |
mcp-connector |
Connects to MCP servers | Stripe, Zendesk, remote APIs |
retriever |
Data retrieval capabilities | Knowledge bases, search |
playbook |
Workflow automation | Pre-defined workflows |
document_importer |
Document import capabilities | Notion, Google Docs, CMS |
system |
Core system plugins | Internal platform features |
Categories#
For marketplace organization:
integration- External service integrationschat- Chat and communicationanalytics- Analytics and insightsautomation- Automation toolsutility- Utility functions
MCP Integration#
Connection Types#
Local MCP Server#
Plugin hosts its own MCP server:
{
"capabilities": {
"mcp": {
"connection": {
"type": "local"
},
"serverPath": "./mcp/index.js",
"transport": "sse|websocket|http",
"startCommand": "node mcp/index.js"
}
}
}
The MCP server:
- Runs as a child process
- Receives environment variables from configuration
- Communicates via specified transport
- Auto-restarts on failure
Remote MCP Server#
Connect to external MCP server:
{
"capabilities": {
"mcp": {
"connection": {
"type": "remote",
"url": "https://mcp.stripe.com"
},
"transport": "http",
"auth": {
"methods": ["oauth2"]
}
}
}
}
Tool Definition#
Each tool must specify:
{
"name": "create_product", // Function identifier
"label": "Create Product", // Human-readable name
"description": "Creates a new product in the store",
"input_schema": {
// JSON Schema
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Product title"
},
"price": {
"type": "number",
"description": "Product price",
"minimum": 0
}
},
"required": ["title", "price"]
}
}
Transport Protocols#
- stdio: Standard input/output (for local servers)
- sse: Server-Sent Events (HTTP streaming)
- websocket: WebSocket connection
- http: HTTP request/response
Multiple transports: "sse|websocket|http"
Plugin Lifecycle#
Discovery and Registration#
- Server Startup: Plugin Manager scans directories
- Manifest Loading: Reads and validates manifest.json
- Schema Validation: Validates against plugin-manifest.schema.json
- Registry Update: Upserts plugin in database with checksum
- Router Loading: Auto-activated plugins load tRPC routers
Installation Flow#
When a plugin is enabled:
graph TD
A[Enable Plugin] --> B{Needs Install?}
B -->|Yes| C[Run installCommand]
B -->|No| D{Needs Build?}
C --> D
D -->|Yes| E[Run buildCommand]
D -->|No| F[Create Instance]
E --> F
F --> G[Plugin Enabled]
Instance Management#
On-Demand Startup#
Instances start when first needed:
- API Request: Tool invocation requested
- Check Running: Is instance already running?
- Pool Check: Are we at max concurrent instances?
- Start Process: Launch MCP server with config
- Activity Track: Record startup timestamp
- Ready: Instance available for use
Inactivity Cleanup#
Background job runs every minute:
- Threshold: 5 minutes of inactivity
- Detection: No tool invocations in timeframe
- Action: Stop MCP server process
- Restart: Next request starts instance again
Pool Limits#
- Default: 10 concurrent instances per plugin
- Configurable: Via
maxConcurrentInstancesin registry - Queueing: Requests wait for available slot (30s timeout)
Configuration Updates#
When configuration changes:
- Update Database: Save encrypted configuration
- Restart Required: MCP server must restart to pick up changes
- Stop Instance: If running, stop the instance
- Next Request: Will start with new configuration
API Reference#
tRPC Endpoints#
All plugin APIs are under /v1/plugins:
Get All Plugins#
const plugins = await Hay.plugins.getAll.query();
// Returns:
interface Plugin {
id: string; // Plugin ID
dbId: number; // Database ID
name: string; // Display name
version: string; // Version
type: string[]; // Plugin types
description: string; // Description
installed: boolean; // Installation status
built: boolean; // Build status
enabled: boolean; // Enabled for this org
hasConfiguration: boolean; // Has configSchema
hasCustomUI: boolean; // Has UI components
capabilities: object; // Capabilities object
sourceType: "core" | "custom";
isCustom: boolean;
}
Get Plugin Details#
const plugin = await Hay.plugins.get.query({
pluginId: "hay-plugin-shopify",
});
Enable Plugin#
const result = await Hay.plugins.enable.mutate({
pluginId: "hay-plugin-shopify",
configuration: {
shopifyAccessToken: "shpat_xxxxx",
myshopifyDomain: "my-store.myshopify.com",
},
});
Disable Plugin#
await Hay.plugins.disable.mutate({
pluginId: "hay-plugin-shopify",
});
Update Configuration#
await Hay.plugins.configure.mutate({
pluginId: "hay-plugin-shopify",
configuration: {
shopifyAccessToken: "new-token",
},
});
Get Plugin Details (includes configuration)#
const plugin = await Hay.plugins.get.query({
pluginId: "hay-plugin-shopify",
});
// Returns plugin details including current configuration
Test Connection#
const result = await Hay.plugins.testConnection.query({
pluginId: "hay-plugin-shopify",
});
// Returns:
interface TestConnectionResult {
success: boolean;
status: "healthy" | "unhealthy" | "unconfigured";
message?: string;
error?: string;
testedAt: Date;
}
Upload Custom Plugin#
const formData = new FormData();
formData.append("plugin", zipFile);
const result = await fetch("/v1/plugins/upload", {
method: "POST",
body: formData,
headers: {
Authorization: `Bearer ${token}`,
"x-organization-id": organizationId,
},
});
Service APIs#
Plugin Manager#
import { pluginManagerService } from "@server/services/plugin-manager.service";
// Get all plugins
const plugins = pluginManagerService.getAllPlugins();
// Get specific plugin
const plugin = pluginManagerService.getPlugin("hay-plugin-shopify");
// Install plugin
await pluginManagerService.installPlugin("hay-plugin-shopify");
// Build plugin
await pluginManagerService.buildPlugin("hay-plugin-shopify");
// Check if needs installation/build
const needsInstall = pluginManagerService.needsInstallation("hay-plugin-shopify");
const needsBuild = pluginManagerService.needsBuilding("hay-plugin-shopify");
Plugin Instance Manager#
import { pluginInstanceManagerService } from "@server/services/plugin-instance-manager.service";
// Ensure instance is running (starts on-demand)
await pluginInstanceManagerService.ensureInstanceRunning(organizationId, "hay-plugin-shopify");
// Update activity timestamp (keeps instance alive)
await pluginInstanceManagerService.updateActivityTimestamp(organizationId, "hay-plugin-shopify");
// Get statistics
const stats = await pluginInstanceManagerService.getStatistics();
// Stop all instances for an organization
await pluginInstanceManagerService.stopAllForOrganization(organizationId);
Plugin Runner#
import { getPluginRunnerService } from "@server/services/plugin-runner.service";
const pluginRunnerService = getPluginRunnerService();
// Start plugin worker
await pluginRunnerService.startWorker(organizationId, "hay-plugin-shopify");
// Stop plugin worker
await pluginRunnerService.stopWorker(organizationId, "hay-plugin-shopify");
// Check if running
const isRunning = pluginRunnerService.isRunning(organizationId, "hay-plugin-shopify");
// Get all workers
const workers = pluginRunnerService.getAllWorkers();
Configuration Management#
Configuration Flow#
- User Input: User enters configuration in dashboard
- Validation: Backend validates against configSchema
- Encryption: Sensitive fields (encrypted: true) are encrypted
- Storage: Stored in plugin_instances.config JSONB
- Environment Variables: Mapped to env vars when starting MCP server
Encryption#
Fields marked encrypted: true are:
- Encrypted at rest using AES-256-GCM
- Decrypted only when needed
- Never exposed in API responses
- Injected as environment variables to MCP server
Environment Variable Mapping#
Configuration fields map to environment variables:
{
"configSchema": {
"apiKey": {
"type": "string",
"env": "SHOPIFY_API_KEY", // Maps to SHOPIFY_API_KEY
"encrypted": true
}
}
}
When the MCP server starts:
SHOPIFY_API_KEY=decrypted_value node mcp/index.js
Best Practices#
- Always encrypt secrets: API keys, tokens, passwords
- Use descriptive env var names: Match the service's conventions
- Provide defaults: For non-sensitive configuration
- Validate input: Use regex patterns for format validation
- Clear descriptions: Help users understand what's needed
UI Extensions#
Settings Extensions#
Add custom UI to plugin settings pages using Vue components.
Before/After Settings#
Add content before or after the main settings form:
{
"settingsExtensions": [
{
"slot": "after-settings",
"component": "components/settings/Instructions.vue"
}
]
}
Custom Tabs#
Add entire tabs to settings:
{
"settingsExtensions": [
{
"slot": "tab",
"component": "components/settings/AdvancedSettings.vue",
"tabName": "Advanced",
"tabOrder": 1
}
]
}
Component Access#
Your Vue components have access to:
OAuth Connection Component#
For OAuth-enabled plugins, use the OAuthConnection component:
Internationalization (i18n)#
Plugins can provide translations for their display name, description, tool labels, and configuration field labels. The system loads translations automatically from the plugin's i18n/ directory — no manifest changes are needed.
Directory Structure#
my-plugin/
├── i18n/
│ ├── en.json # English (always required — used as fallback)
│ └── pt-BR.json # Brazilian Portuguese
├── src/
├── mcp/
└── ...
File names are locale codes (e.g., en.json, pt-BR.json, es.json). The system supports any locale — just add a new JSON file.
Translation File Format#
Each JSON file has four top-level keys:
{
"name": "My Plugin",
"description": "What this plugin does in one sentence",
"tools": {
"tool_name": {
"label": "Human-Readable Tool Name",
"description": "What this tool does"
}
},
"config": {
"configFieldName": {
"label": "Field Label",
"description": "Help text shown to the user"
}
}
}
| Key | Purpose |
|---|---|
name |
Plugin display name in marketplace and settings |
description |
Plugin summary shown in marketplace cards |
tools |
Labels and descriptions for each MCP tool (keys must match tool IDs from the MCP server) |
config |
Labels and descriptions for each config field (keys must match configSchema field names) |
All keys are optional — only translate what applies to your plugin.
Example: English (i18n/en.json)#
{
"name": "Stripe",
"description": "Connect your Stripe account to manage payments, customers, subscriptions, and more",
"tools": {
"list_customers": {
"label": "List Customers",
"description": "Search and list Stripe customers"
},
"create_payment": {
"label": "Create Payment",
"description": "Create a new payment intent"
}
},
"config": {
"apiKey": {
"label": "API Key",
"description": "Stripe API key (starts with sk_test_ or sk_live_)"
}
}
}
Example: Portuguese (i18n/pt-BR.json)#
{
"name": "Stripe",
"description": "Conecte sua conta Stripe para gerenciar pagamentos, clientes, assinaturas e mais",
"tools": {
"list_customers": {
"label": "Listar Clientes",
"description": "Buscar e listar clientes do Stripe"
},
"create_payment": {
"label": "Criar Pagamento",
"description": "Criar uma nova intenção de pagamento"
}
},
"config": {
"apiKey": {
"label": "Chave de API",
"description": "Chave de API do Stripe (começa com sk_test_ ou sk_live_)"
}
}
}
How It Works#
-
Loading: At startup, the Plugin Manager scans each plugin's
i18n/directory, reads all.jsonfiles, and attaches them to the plugin manifest under thei18nproperty. -
Serving: The frontend requests translations for the current locale via
Hay.plugins.getPluginTranslations.query({ locale }). The backend returns translations for all plugins, falling back toenif the requested locale is missing. -
Rendering: The frontend merges plugin translations into Vue I18n under the
pluginsnamespace. Tool labels, config labels, and plugin names are then resolved from these translations in the UI.
Fallback Behavior#
- If a requested locale (e.g.,
pt-BR) has no translation file, the system falls back toen.json. - If a plugin has no
i18n/directory at all, the raw values from the MCP server and manifest are used directly. - Always provide
en.jsonas a baseline — it ensures every string has a readable default.
Currently Supported Locales#
en— Englishpt-BR— Brazilian Portuguese
Additional locales can be added at any time by creating a new JSON file.
Type Definition#
interface PluginLocale {
name?: string;
description?: string;
tools?: Record<string, { label: string; description?: string }>;
config?: Record<string, { label: string; description?: string }>;
}
Best Practices#
- Always include
en.json— it's the fallback for all missing locales. - Keep tool keys in sync — the keys in
toolsmust match the tool names registered in your MCP server. - Keep config keys in sync — the keys in
configmust match the field names in yourconfigSchemaorctx.register.config(). - Use clear, concise labels — tool labels appear in the AI agent's tool picker and in the dashboard.
- Don't translate tool IDs — only translate the
labelanddescriptionvalues, never the keys.
Channel Registration#
Plugins can register custom communication channels (sources) for handling messages from various platforms.
Source Model#
interface Source {
id: string; // Unique identifier
name: string; // Display name
description?: string; // Channel description
category: SourceCategory; // Category enum
pluginId?: string; // Plugin that registered this
isActive: boolean; // Active status
icon?: string; // Icon identifier
metadata?: Record<string, unknown>; // Plugin-specific config
}
enum SourceCategory {
TEST = "test",
MESSAGING = "messaging",
SOCIAL = "social",
EMAIL = "email",
HELPDESK = "helpdesk",
}
Registering a Source#
// In your plugin initialization
const source = await trpc.sources.register.mutate({
id: "whatsapp", // or 'my-plugin:whatsapp'
name: "WhatsApp Business",
description: "WhatsApp Business API integration",
category: "messaging",
pluginId: "whatsapp-plugin",
icon: "whatsapp",
metadata: {
apiVersion: "2.0",
capabilities: ["text", "media", "templates"],
},
});
Source ID Conventions#
- Simple format:
whatsapp,telegram- for well-known channels - Namespaced:
my-plugin:custom-channel- for custom channels - Pattern:
/^[a-z0-9_:-]+$/- lowercase, alphanumeric, dashes, underscores, colons
Creating Messages with Sources#
When creating messages from your plugin, specify the sourceId:
import { MessageService } from "@server/services/core/message.service";
const messageService = new MessageService();
// Create message with test mode handling
const message = await messageService.createAssistantMessageWithTestMode(
conversation,
"Hello from WhatsApp!",
"whatsapp", // sourceId
agent,
organization,
{
whatsappMessageId: "wamid.xxxxx",
phoneNumber: "+1234567890",
},
);
// Check delivery state
if (message.deliveryState === "queued") {
// Message needs approval (test mode)
console.log("Message queued for approval");
} else {
// Message approved automatically
await sendToExternalPlatform(message);
}
Source Management#
// Deactivate source
await trpc.sources.deactivate.mutate({ id: "whatsapp" });
// Activate source
await trpc.sources.activate.mutate({ id: "whatsapp" });
// List all sources
const sources = await trpc.sources.list.query();
// Get by category
const messagingSources = await trpc.sources.getByCategory.query({
category: "messaging",
});
See docs/technical/plugins/channel-registration.md for detailed guide.
Best Practices#
Development#
- Follow the schema: Always validate your manifest against the JSON schema
- Version properly: Use semantic versioning (MAJOR.MINOR.PATCH)
- Document tools: Provide clear descriptions for all MCP tools
- Handle errors: Implement proper error handling in MCP servers
- Test thoroughly: Test installation, configuration, and tool invocation
Security#
- Encrypt secrets: Mark all sensitive fields as encrypted
- Validate input: Use regex patterns and type validation
- Least privilege: Request only necessary OAuth scopes
- Secure communication: Use HTTPS for remote MCP servers
- Never log secrets: Don't log configuration or tokens
Performance#
- Optimize startup: Fast MCP server startup improves UX
- Handle inactivity: Design for automatic shutdown after 5 minutes
- Efficient tools: Keep tool execution fast (<5 seconds ideal)
- Cache when possible: Cache expensive operations
- Monitor resources: Be mindful of memory and CPU usage
Architecture#
- Don't hardcode plugin IDs: Use dynamic discovery
- Respect organization boundaries: Custom plugins are org-scoped
- Follow MCP standards: Implement proper MCP protocol
- Use TypeScript: Type safety prevents runtime errors
- Modular design: Keep plugins focused and single-purpose
Documentation#
- Clear README: Document setup and configuration
- Example usage: Provide example tool invocations
- Troubleshooting: Common issues and solutions
- API documentation: Document all tools and parameters
- Update CLAUDE.md: Add plugin-specific guidance if needed
Building New Features#
Adding a New Plugin Type#
- Update Schema: Add new type to the plugin types definition in
server/types/plugin.types.ts - Update Types: Add to TypeScript types in
server/types/plugin.types.ts - Handle in Code: Update plugin manager to handle new type
- Document: Add to this documentation
Adding New Plugin Capabilities#
- Define Schema: Add capability to the plugin manifest structure
- Implement Handler: Create service to handle the capability
- Add API: Expose via tRPC if needed
- Test: Create test plugin using the capability
- Document: Add to capabilities section
Adding Plugin API Permissions#
The permissions.api field uses the Plugin API pattern - plugins can only access platform APIs they explicitly declare:
- Define API: Create the API service (e.g., EmailService)
- Add to Schema: Add to
permissions.apienum in plugin types - Check Permission: Verify plugin has permission before allowing access
- Document: Add to API reference
Example:
{
"permissions": {
"api": ["email", "sms"] // Plugin can access email and SMS APIs
}
}
Extending MCP Support#
- New Transport: Implement transport in MCP client factory
- New Auth Method: Add to auth handling in process manager
- Tool Enhancements: Update tool invocation logic
- Test: Verify with real MCP server
- Document: Update MCP integration section
Custom tRPC Routers#
Plugins can provide their own tRPC routers:
- Create Router: Export router from plugin
// plugins/my-plugin/dist/router.js
export const router = trpcRouter({
myEndpoint: publicProcedure.query(() => {
return { message: "Hello from plugin" };
}),
});
- Reference in Manifest:
{
"autoActivate": true,
"trpcRouter": "./dist/router.js"
}
- Access: Call via
Hay.plugins.myPlugin.myEndpoint.query()
Troubleshooting#
Plugin Not Appearing#
Problem: Plugin not visible in marketplace
Solutions:
- Check manifest.json exists and is valid JSON
- Verify plugin ID matches pattern
/^[a-z0-9-]+$/ - Check
invisiblefield is nottrue - For custom plugins, verify in correct org directory
- Check server logs for validation errors
Installation Failures#
Problem: Plugin installation fails
Solutions:
- Check
installCommandin manifest - Verify package.json exists in plugin directory
- Ensure network access for npm install
- Check for native dependency issues
- Review installation logs in console
Build Failures#
Problem: Plugin build fails
Solutions:
- Check
buildCommandin manifest - Verify TypeScript configuration
- Check for compilation errors
- Ensure all dependencies installed
- Review build logs
MCP Server Won't Start#
Problem: Plugin instance fails to start
Solutions:
- Verify
startCommandis correct - Check MCP server code for errors
- Ensure required environment variables set
- Check port availability
- Review process manager logs
- Verify file permissions
Tool Invocation Errors#
Problem: Tool calls fail or timeout
Solutions:
- Verify tool name matches manifest
- Check input_schema validation
- Ensure MCP server is running
- Check tool implementation for errors
- Verify authentication credentials
- Check network connectivity for remote servers
Configuration Not Working#
Problem: Configuration changes not taking effect
Solutions:
- Stop and restart the plugin instance
- Verify configuration saved in database
- Check environment variable mapping
- Ensure encrypted fields are decrypting
- Review MCP server startup logs
OAuth Authentication Issues#
Problem: OAuth flow fails
Solutions:
- Verify OAuth URLs are correct
- Check client ID and secret configuration
- Ensure redirect URI is registered
- Verify scopes are valid
- Check PKCE requirements
- Review OAuth provider logs
Additional Resources#
- Plugin Generation: See
.claude/PLUGIN_GENERATION_WORKFLOW.md - Channel Registration: See
docs/technical/plugins/channel-registration.md - Database Conventions: See
server/database/DATABASE_CONVENTIONS.md - Frontend Guidelines: See
.claude/FRONTEND.md - Example Plugins: Browse
plugins/core/directory
Getting Help#
- Issues: Open an issue in the repository
- Examples: Study existing plugins in
plugins/core/ - Community: Join the Hay developer community
- Documentation: This guide and related docs in
/docs
Last Updated: 2025-12-03
Version: 1.0.0