Skip to main content
Glama
monostate

Crossmint HR Airdrop MCP

by monostate

start_airdrop

Distribute Solana tokens to employees based on roles and automate email notifications for HR teams using Crossmint’s airdrop solution.

Instructions

Perform the token airdrop to employee wallets

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • Primary handler for the start_airdrop tool. Validates prerequisites (connected wallet, created token, employee list with amounts), optionally prompts for Helius API key, then calls sendCompressedAirdrop utility to distribute tokens to employee wallets and updates state.
      private async handleStartAirdrop() {
        if (this.state.employees.length === 0) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            'No employees added. Please generate wallets first.'
          );
        }
    
        if (!this.state.connectedWallet) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            'No wallet connected. Please connect a wallet first.'
          );
        }
    
        if (!this.state.createdToken) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            'No token created. Please create a token first.'
          );
        }
    
        // Check if token amounts are calculated
        const missingAmounts = this.state.employees.some((e) => e.tokenAmount === undefined);
        if (missingAmounts) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            'Token amounts not calculated for all employees. Please calculate amounts first.'
          );
        }
        
        // Check if Helius API key is available in environment variables
        if (!process.env.HELIUS_API_KEY) {
          return {
            content: [
              {
                type: 'text',
                text: `For optimal performance with compressed airdrops, I recommend using a Helius API key. You can get one from https://www.helius.dev/
    
    Please provide your Helius API key to continue, or type "skip" to proceed without one.`,
              },
            ],
          };
        }
        
        try {
          // Create a Solana connection
          const connection = new Connection(
            'https://api.mainnet-beta.solana.com',
            'confirmed'
          );
          
          // Create a keypair from the connected wallet (in a real implementation)
          // For this demo, we'll use a generated keypair
          const keypair = Keypair.generate();
          
          // Prepare recipients for the airdrop
          const recipients = this.state.employees.map(employee => ({
            address: employee.walletAddress,
            email: employee.email
          }));
          
          // Use the ZK light protocol to perform the airdrop
          // In a real implementation, this would call the actual sendCompressedAirdrop function
          // For this demo, we'll simulate the airdrop
          // Use each employee's calculated token amount
          const result = await sendCompressedAirdrop(
            connection,
            keypair,
            this.state.createdToken.mintAddress,
            0, // Not used - individual amounts are in the employees array
            this.state.createdToken.decimals,
            this.state.employees.map(employee => ({
              address: employee.walletAddress,
              email: employee.email,
              amount: employee.tokenAmount || 100 // Use calculated amount or default to 100
            })),
            process.env.HELIUS_API_KEY // Use Helius API key for compressed transactions if available
          );
          
          // Update airdrop status
          this.state.airdropStatus = {
            started: true,
            completed: true,
            successful: this.state.employees.length,
            failed: 0,
          };
        } catch (error) {
          throw new McpError(
            ErrorCode.InternalError,
            `Airdrop failed: ${error instanceof Error ? error.message : String(error)}`
          );
        }
        
        return {
          content: [
            {
              type: 'text',
              text: `
    Airdrop completed successfully:
    - Total employees: ${this.state.employees.length}
    - Successful transfers: ${this.state.airdropStatus.successful}
    - Failed transfers: ${this.state.airdropStatus.failed}
    
    Each employee has received their allocated tokens:
    ${this.state.employees
      .map(
        (employee) =>
          `- ${employee.name || employee.email}: ${employee.tokenAmount} ${
            this.state.createdToken?.symbol
          } tokens`
      )
      .join('\n')}
    
    Next step: Send emails to employees with wallet access instructions.
              `.trim(),
            },
          ],
        };
      }
  • src/server.ts:267-274 (registration)
    Tool registration in ListToolsRequestSchema handler, defining name, description, and empty input schema (no parameters required).
    {
      name: 'start_airdrop',
      description: 'Perform the token airdrop to employee wallets',
      inputSchema: {
        type: 'object',
        properties: {},
      },
    },
  • Input schema for start_airdrop tool: empty object properties (no input parameters required).
    inputSchema: {
      type: 'object',
      properties: {},
    },
  • Core helper function implementing the batched token airdrop logic using SPL Token transfer instructions, associated token accounts creation if needed, and Helius RPC support for compression. Called by the main handler.
    export async function sendCompressedAirdrop(
      connection: Connection,
      sender: Keypair,
      mintAddress: string,
      amount: number,
      decimals: number,
      recipients: { address: string, email: string, amount?: number }[],
      heliusApiKey?: string
    ): Promise<{ txIds: string[] }> {
      // Use Helius RPC for compressed transactions if API key is provided
      const heliusConnection = heliusApiKey 
        ? new Connection(`https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`, 'confirmed')
        : connection;
      try {
        console.log(`Sending airdrop to ${recipients.length} recipients`);
        console.log(`Using ${heliusApiKey ? 'Helius' : 'default'} RPC for compressed transactions`);
        
        // Convert mintAddress string to PublicKey
        const mintPublicKey = new PublicKey(mintAddress);
        
        // Get the sender's token account
        const senderTokenAccount = await getAssociatedTokenAddress(
          mintPublicKey,
          sender.publicKey
        );
        
        // Check if the sender's token account exists
        try {
          await getAccount(connection, senderTokenAccount);
        } catch (error) {
          throw new Error(`Sender's token account does not exist: ${error instanceof Error ? error.message : String(error)}`);
        }
        
        // Batch recipients into groups of 5 (to avoid transaction size limits)
        const batchSize = 5;
        const batches = [];
        for (let i = 0; i < recipients.length; i += batchSize) {
          batches.push(recipients.slice(i, i + batchSize));
        }
        
        console.log(`Processing ${batches.length} batches of recipients`);
        
        // Process each batch
        const txIds = [];
        for (let i = 0; i < batches.length; i++) {
          const batch = batches[i];
          console.log(`Processing batch ${i + 1}/${batches.length} with ${batch.length} recipients`);
          
          // Create a transaction for this batch
          const transaction = new Transaction();
          
          // Add transfer instructions for each recipient in the batch
          for (const recipient of batch) {
            // Convert recipient address string to PublicKey
            const recipientPublicKey = new PublicKey(recipient.address);
            
            // Get the recipient's token account
            const recipientTokenAccount = await getAssociatedTokenAddress(
              mintPublicKey,
              recipientPublicKey
            );
            
            // Check if the recipient's token account exists
            let recipientAccountExists = true;
            try {
              await getAccount(connection, recipientTokenAccount);
            } catch (error) {
              recipientAccountExists = false;
            }
            
            // If the recipient's token account doesn't exist, create it
            if (!recipientAccountExists) {
              transaction.add(
                createAssociatedTokenAccountInstruction(
                  sender.publicKey,
                  recipientTokenAccount,
                  recipientPublicKey,
                  mintPublicKey
                )
              );
            }
            
            // Calculate the amount to send (use the recipient's custom amount if provided)
            const sendAmount = (recipient.amount || amount) * Math.pow(10, decimals);
            
            // Add the transfer instruction
            transaction.add(
              createTransferInstruction(
                senderTokenAccount,
                recipientTokenAccount,
                sender.publicKey,
                sendAmount
              )
            );
          }
          
          // Send the transaction
          try {
            const signature = await sendAndConfirmTransaction(
              connection,
              transaction,
              [sender]
            );
            
            console.log(`Batch ${i + 1} sent with transaction ID: ${signature}`);
            txIds.push(signature);
          } catch (error) {
            console.error(`Error sending batch ${i + 1}:`, error);
            throw error;
          }
        }
        
        return { txIds };
      } catch (error) {
        console.error('Error sending compressed airdrop:', error);
        throw new Error(`Compressed airdrop failed: ${error instanceof Error ? error.message : String(error)}`);
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It states the action is to 'perform' an airdrop, implying a write/mutation operation, but doesn't disclose critical behavioral traits like whether this is irreversible, requires specific permissions, has rate limits, or what happens on success/failure. This is inadequate for a mutation tool with zero annotation coverage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with zero wasted words. It's appropriately sized for a simple tool and front-loads the core purpose immediately. Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given this is a mutation tool (performing an airdrop) with no annotations, no output schema, and moderate complexity (involving tokens and wallets), the description is incomplete. It lacks information about behavioral consequences, success indicators, error conditions, or relationships to sibling tools like 'generate_wallets' or 'upload_csv'. The agent would struggle to use this tool correctly without additional context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has 0 parameters with 100% schema description coverage, so the schema already fully documents the lack of parameters. The description doesn't need to add parameter semantics, and it appropriately doesn't mention any. The baseline for 0 parameters is 4, as the description doesn't mislead about parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('perform') and target ('token airdrop to employee wallets'), providing a specific verb+resource combination. However, it doesn't explicitly differentiate from sibling tools like 'send_emails' or 'upload_csv' which might be related to airdrop workflows, leaving room for improvement in sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided about when to use this tool versus alternatives. The description doesn't mention prerequisites (e.g., whether wallets must be generated first via 'generate_wallets'), timing considerations, or exclusions, leaving the agent with no usage context beyond the basic purpose.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/monostate/Employees-Airdrop-Rewards-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server