Login Get started

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#

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#

  1. Dynamic Loading: Plugins are discovered and loaded at runtime
  2. Organization Isolation: Custom plugins are scoped to organizations
  3. On-Demand Activation: Plugin instances start only when needed
  4. MCP-First: Built around the Model Context Protocol standard
  5. 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.json with a hay-plugin block rather than a separate plugins/base/plugin-manifest.schema.json schema 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 integrations
  • chat - Chat and communication
  • analytics - Analytics and insights
  • automation - Automation tools
  • utility - 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#

  1. Server Startup: Plugin Manager scans directories
  2. Manifest Loading: Reads and validates manifest.json
  3. Schema Validation: Validates against plugin-manifest.schema.json
  4. Registry Update: Upserts plugin in database with checksum
  5. 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:

  1. API Request: Tool invocation requested
  2. Check Running: Is instance already running?
  3. Pool Check: Are we at max concurrent instances?
  4. Start Process: Launch MCP server with config
  5. Activity Track: Record startup timestamp
  6. 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 maxConcurrentInstances in registry
  • Queueing: Requests wait for available slot (30s timeout)

Configuration Updates#

When configuration changes:

  1. Update Database: Save encrypted configuration
  2. Restart Required: MCP server must restart to pick up changes
  3. Stop Instance: If running, stop the instance
  4. 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#

  1. User Input: User enters configuration in dashboard
  2. Validation: Backend validates against configSchema
  3. Encryption: Sensitive fields (encrypted: true) are encrypted
  4. Storage: Stored in plugin_instances.config JSONB
  5. 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#

  1. Always encrypt secrets: API keys, tokens, passwords
  2. Use descriptive env var names: Match the service's conventions
  3. Provide defaults: For non-sensitive configuration
  4. Validate input: Use regex patterns for format validation
  5. 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#

  1. Loading: At startup, the Plugin Manager scans each plugin's i18n/ directory, reads all .json files, and attaches them to the plugin manifest under the i18n property.

  2. Serving: The frontend requests translations for the current locale via Hay.plugins.getPluginTranslations.query({ locale }). The backend returns translations for all plugins, falling back to en if the requested locale is missing.

  3. Rendering: The frontend merges plugin translations into Vue I18n under the plugins namespace. 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 to en.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.json as a baseline — it ensures every string has a readable default.

Currently Supported Locales#

  • en — English
  • pt-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#

  1. Always include en.json — it's the fallback for all missing locales.
  2. Keep tool keys in sync — the keys in tools must match the tool names registered in your MCP server.
  3. Keep config keys in sync — the keys in config must match the field names in your configSchema or ctx.register.config().
  4. Use clear, concise labels — tool labels appear in the AI agent's tool picker and in the dashboard.
  5. Don't translate tool IDs — only translate the label and description values, 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#

  1. Follow the schema: Always validate your manifest against the JSON schema
  2. Version properly: Use semantic versioning (MAJOR.MINOR.PATCH)
  3. Document tools: Provide clear descriptions for all MCP tools
  4. Handle errors: Implement proper error handling in MCP servers
  5. Test thoroughly: Test installation, configuration, and tool invocation

Security#

  1. Encrypt secrets: Mark all sensitive fields as encrypted
  2. Validate input: Use regex patterns and type validation
  3. Least privilege: Request only necessary OAuth scopes
  4. Secure communication: Use HTTPS for remote MCP servers
  5. Never log secrets: Don't log configuration or tokens

Performance#

  1. Optimize startup: Fast MCP server startup improves UX
  2. Handle inactivity: Design for automatic shutdown after 5 minutes
  3. Efficient tools: Keep tool execution fast (<5 seconds ideal)
  4. Cache when possible: Cache expensive operations
  5. Monitor resources: Be mindful of memory and CPU usage

Architecture#

  1. Don't hardcode plugin IDs: Use dynamic discovery
  2. Respect organization boundaries: Custom plugins are org-scoped
  3. Follow MCP standards: Implement proper MCP protocol
  4. Use TypeScript: Type safety prevents runtime errors
  5. Modular design: Keep plugins focused and single-purpose

Documentation#

  1. Clear README: Document setup and configuration
  2. Example usage: Provide example tool invocations
  3. Troubleshooting: Common issues and solutions
  4. API documentation: Document all tools and parameters
  5. Update CLAUDE.md: Add plugin-specific guidance if needed

Building New Features#

Adding a New Plugin Type#

  1. Update Schema: Add new type to the plugin types definition in server/types/plugin.types.ts
  2. Update Types: Add to TypeScript types in server/types/plugin.types.ts
  3. Handle in Code: Update plugin manager to handle new type
  4. Document: Add to this documentation

Adding New Plugin Capabilities#

  1. Define Schema: Add capability to the plugin manifest structure
  2. Implement Handler: Create service to handle the capability
  3. Add API: Expose via tRPC if needed
  4. Test: Create test plugin using the capability
  5. 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:

  1. Define API: Create the API service (e.g., EmailService)
  2. Add to Schema: Add to permissions.api enum in plugin types
  3. Check Permission: Verify plugin has permission before allowing access
  4. Document: Add to API reference

Example:

{
  "permissions": {
    "api": ["email", "sms"] // Plugin can access email and SMS APIs
  }
}

Extending MCP Support#

  1. New Transport: Implement transport in MCP client factory
  2. New Auth Method: Add to auth handling in process manager
  3. Tool Enhancements: Update tool invocation logic
  4. Test: Verify with real MCP server
  5. Document: Update MCP integration section

Custom tRPC Routers#

Plugins can provide their own tRPC routers:

  1. Create Router: Export router from plugin
// plugins/my-plugin/dist/router.js
export const router = trpcRouter({
  myEndpoint: publicProcedure.query(() => {
    return { message: "Hello from plugin" };
  }),
});
  1. Reference in Manifest:
{
  "autoActivate": true,
  "trpcRouter": "./dist/router.js"
}
  1. 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 invisible field is not true
  • For custom plugins, verify in correct org directory
  • Check server logs for validation errors

Installation Failures#

Problem: Plugin installation fails

Solutions:

  • Check installCommand in 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 buildCommand in 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 startCommand is 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

Last updated June 22, 2026

View on GitHub