Skip to main content
Glama
server.py9.77 kB
""" Instantly MCP Server - Main Server A lightweight, robust FastMCP server for the Instantly.ai V2 API. Features: - 38 tools across 6 categories (accounts, campaigns, leads, emails, analytics, background_jobs) - Dual transport support (HTTP for remote, stdio for local) - Lazy loading via TOOL_CATEGORIES environment variable - Per-request API key support for multi-tenant deployments - MCP 2025-06-18 annotations (readOnlyHint, destructiveHint, etc.) Usage: # HTTP mode (remote deployment) fastmcp run src/instantly_mcp/server.py --transport http --port 8000 # stdio mode (local Claude Desktop) fastmcp run src/instantly_mcp/server.py # With lazy loading (reduce context window) TOOL_CATEGORIES=accounts,campaigns fastmcp run src/instantly_mcp/server.py """ import os import sys from typing import Any from fastmcp import FastMCP from fastmcp.server import Context from .client import get_client, set_api_key from .tools import get_all_tools, get_requested_categories, is_lazy_loading_enabled # Server metadata SERVER_NAME = "instantly-mcp" SERVER_VERSION = "1.0.0" SERVER_INSTRUCTIONS = """ Instantly.ai V2 API MCP Server - Email automation and campaign management. Categories: accounts, campaigns, leads, emails, analytics, background_jobs Total tools: 38 (configurable via TOOL_CATEGORIES env var) Authentication methods for HTTP deployments: 1. URL path: /mcp/YOUR_API_KEY 2. Header: Authorization: YOUR_API_KEY (Bearer prefix optional) 3. Header: x-instantly-api-key: YOUR_API_KEY """ # Initialize FastMCP server mcp = FastMCP( name=SERVER_NAME, version=SERVER_VERSION, instructions=SERVER_INSTRUCTIONS, ) def register_tools(): """Register all tools with MCP annotations.""" # Import tool modules dynamically based on categories tools = get_all_tools() # Tool annotations mapping # readOnlyHint: Tool only reads data # destructiveHint: Tool modifies/deletes data # confirmationRequiredHint: Requires user confirmation TOOL_ANNOTATIONS = { # Account tools "list_accounts": {"readOnlyHint": True}, "get_account": {"readOnlyHint": True}, "create_account": {"destructiveHint": False}, "update_account": {"destructiveHint": False}, "manage_account_state": {"destructiveHint": False}, "delete_account": {"destructiveHint": True, "confirmationRequiredHint": True}, # Campaign tools "create_campaign": {"destructiveHint": False}, "list_campaigns": {"readOnlyHint": True}, "get_campaign": {"readOnlyHint": True}, "update_campaign": {"destructiveHint": False}, "activate_campaign": {"destructiveHint": False}, "pause_campaign": {"destructiveHint": False}, "delete_campaign": {"destructiveHint": True, "confirmationRequiredHint": True}, "search_campaigns_by_contact": {"readOnlyHint": True}, # Lead tools "list_leads": {"readOnlyHint": True}, "get_lead": {"readOnlyHint": True}, "create_lead": {"destructiveHint": False}, "update_lead": {"destructiveHint": False}, "list_lead_lists": {"readOnlyHint": True}, "create_lead_list": {"destructiveHint": False}, "update_lead_list": {"destructiveHint": False}, "get_verification_stats_for_lead_list": {"readOnlyHint": True}, "add_leads_to_campaign_or_list_bulk": {"destructiveHint": False}, "delete_lead": {"destructiveHint": True, "confirmationRequiredHint": True}, "delete_lead_list": {"destructiveHint": True, "confirmationRequiredHint": True}, "move_leads_to_campaign_or_list": {"destructiveHint": False}, # Email tools "list_emails": {"readOnlyHint": True}, "get_email": {"readOnlyHint": True}, "reply_to_email": {"destructiveHint": True, "confirmationRequiredHint": True}, "count_unread_emails": {"readOnlyHint": True}, "verify_email": {"readOnlyHint": True}, "mark_thread_as_read": {"destructiveHint": False}, # Analytics tools "get_campaign_analytics": {"readOnlyHint": True}, "get_daily_campaign_analytics": {"readOnlyHint": True}, "get_warmup_analytics": {"readOnlyHint": True}, # Background job tools "list_background_jobs": {"readOnlyHint": True}, "get_background_job": {"readOnlyHint": True}, } for tool_func in tools: tool_name = tool_func.__name__ annotations = TOOL_ANNOTATIONS.get(tool_name, {}) # Register tool with FastMCP mcp.tool( name=tool_name, annotations=annotations, )(tool_func) print(f"[Instantly MCP] ✅ Registered {len(tools)} tools", file=sys.stderr) if is_lazy_loading_enabled(): categories = get_requested_categories() print(f"[Instantly MCP] 📦 Categories: {', '.join(categories)}", file=sys.stderr) # Register tools at import time register_tools() @mcp.tool( name="get_server_info", annotations={"readOnlyHint": True}, ) async def get_server_info() -> str: """ Get Instantly MCP server information. Returns server version, loaded categories, and configuration status. Useful for debugging and verifying server setup. """ import json client = get_client() categories = get_requested_categories() info = { "server": SERVER_NAME, "version": SERVER_VERSION, "api_key_configured": client.has_api_key, "lazy_loading_enabled": is_lazy_loading_enabled(), "loaded_categories": categories, "tool_counts": { "accounts": 6 if "accounts" in categories else 0, "campaigns": 8 if "campaigns" in categories else 0, # +2: delete_campaign, search_campaigns_by_contact "leads": 12 if "leads" in categories else 0, # +1: delete_lead_list "emails": 6 if "emails" in categories else 0, # +1: mark_thread_as_read "analytics": 3 if "analytics" in categories else 0, "background_jobs": 2 if "background_jobs" in categories else 0, }, "total_tools": sum([ 6 if "accounts" in categories else 0, 8 if "campaigns" in categories else 0, 12 if "leads" in categories else 0, 6 if "emails" in categories else 0, 3 if "analytics" in categories else 0, 2 if "background_jobs" in categories else 0, ]) + 1, # +1 for get_server_info "rate_limit": { "remaining": client.rate_limit.remaining, "limit": client.rate_limit.limit, "reset_at": client.rate_limit.reset_at.isoformat() if client.rate_limit.reset_at else None, }, } return json.dumps(info, indent=2) def extract_api_key_from_request(ctx: Context) -> str | None: """ Extract API key from request context for multi-tenant HTTP mode. Checks headers in order: 1. x-instantly-api-key 2. Authorization: Bearer <key> """ if not ctx or not hasattr(ctx, "request"): return None request = ctx.request if not request or not hasattr(request, "headers"): return None headers = request.headers # Check custom header first api_key = headers.get("x-instantly-api-key") if api_key: return api_key # Check Authorization header auth_header = headers.get("authorization", "") if auth_header.lower().startswith("bearer "): return auth_header[7:] return None def main(): """Main entry point for the Instantly MCP server.""" import argparse parser = argparse.ArgumentParser(description="Instantly MCP Server") parser.add_argument( "--transport", choices=["stdio", "http"], default="stdio", help="Transport mode (default: stdio)", ) parser.add_argument( "--host", default=os.environ.get("HOST", "0.0.0.0"), help="HTTP host (default: 0.0.0.0)", ) parser.add_argument( "--port", type=int, default=int(os.environ.get("PORT", "8000")), help="HTTP port (default: 8000)", ) parser.add_argument( "--api-key", help="Instantly API key (overrides INSTANTLY_API_KEY env var)", ) args = parser.parse_args() # Set API key if provided if args.api_key: set_api_key(args.api_key) # Log startup info client = get_client() print(f"[Instantly MCP] 🚀 Starting server v{SERVER_VERSION}", file=sys.stderr) print(f"[Instantly MCP] 🔑 API key: {'✅ Configured' if client.has_api_key else '❌ Not set (multi-tenant mode)'}", file=sys.stderr) print(f"[Instantly MCP] 🚌 Transport: {args.transport}", file=sys.stderr) if args.transport == "http": print(f"[Instantly MCP] 🌐 HTTP endpoints:", file=sys.stderr) print(f"[Instantly MCP] - http://{args.host}:{args.port}/mcp", file=sys.stderr) print(f"[Instantly MCP] - http://{args.host}:{args.port}/mcp/YOUR_API_KEY", file=sys.stderr) if not client.has_api_key: print(f"[Instantly MCP] ⚠️ Multi-tenant mode: Provide API key via URL or headers", file=sys.stderr) # Use custom HTTP app with URL-based auth support from .http_app import run_http_server run_http_server(mcp, host=args.host, port=args.port) else: if not client.has_api_key: print("[Instantly MCP] ❌ API key required for stdio mode", file=sys.stderr) print("[Instantly MCP] Set INSTANTLY_API_KEY env var or use --api-key", file=sys.stderr) sys.exit(1) mcp.run(transport="stdio") if __name__ == "__main__": main()

Implementation Reference

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/bcharleson/instantly-mcp-python'

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