ExMCP Configuration Guide

View Source

This guide covers all configuration options available in ExMCP, from basic setup to advanced production configurations.

Table of Contents

  1. Basic Configuration
  2. Protocol Version Configuration
  3. Transport Configuration
  4. Security Configuration
  5. Logging Configuration
  6. Performance Configuration
  7. Production Configuration
  8. Environment-Specific Configuration

Basic Configuration

Application Configuration

Configure ExMCP in your config/config.exs:

# config/config.exs
config :ex_mcp,
  # Default protocol version
  protocol_version: "2025-11-25",

  # Global timeout settings
  default_timeout: 30_000,
  request_timeout: 15_000,
  
  # Connection settings
  auto_reconnect: true,
  reconnect_backoff: [initial: 1000, max: 30_000],
  
  # Logging level
  log_level: :info

Mix Project Configuration

In your mix.exs:

def deps do
  [
    {:ex_mcp, "~> 0.9.0"}
  ]
end

Protocol Version Configuration

ExMCP supports multiple MCP protocol versions. Choose based on your needs:

Available Versions

config :ex_mcp,
  protocol_version: "2024-11-05"  # Maximum compatibility
  # OR
  protocol_version: "2025-03-26"  # Stable baseline
  # OR
  protocol_version: "2025-06-18"  # Structured output, elicitation, OAuth 2.1
  # OR
  protocol_version: "2025-11-25"  # Latest (recommended)

Version Feature Matrix

Feature2024-11-052025-03-262025-06-182025-11-25
Core Features
Tools, Resources, Prompts
Bi-directional requests
Request cancellation
Progress notifications
2025-03-26 Features
Resource subscriptions--
Roots--
Structured logging--
Tool annotations--
2025-06-18 Features
Structured tool output----
Elicitation support----
OAuth 2.1 Resource Server----
2025-11-25 Features
Tasks (async long-running tools)------experimental
Icons metadata------
URL-mode elicitation------
Tool calling in sampling------
Enhanced OAuth/OIDC (incremental scope)------
JWT client authentication (private_key_jwt)------
Enterprise SSO / ID-JAG------

Recommendations

  • Production: Use "2025-11-25" for the latest features
  • Compatibility: Use "2024-11-05" for maximum compatibility with older clients

Transport Configuration

stdio Transport

For subprocess communication:

# Server configuration
{:ok, server} = ExMCP.Server.start_link(
  handler: MyHandler,
  transport: :stdio
)

# Client configuration
{:ok, client} = ExMCP.Client.start_link(
  transport: :stdio,
  command: ["python", "mcp-server.py"],
  args: ["--config", "prod.json"],
  timeout: 30_000,
  
  # Optional environment variables
  env: [{"PYTHONPATH", "/opt/myapp"}],
  
  # Working directory
  cd: "/path/to/server"
)

HTTP Transport (Streamable HTTP)

For network communication with optional Server-Sent Events:

# Server configuration
{:ok, server} = ExMCP.Server.start_link(
  handler: MyHandler,
  transport: :http,
  port: 8080,
  path: "/mcp",
  
  # SSE configuration
  sse_enabled: true,
  sse_path: "/events",
  
  # Security
  cors_enabled: true,
  allowed_origins: ["https://app.example.com"],
  
  # Connection limits
  max_connections: 1000,
  timeout: 60_000
)

# Client configuration
{:ok, client} = ExMCP.Client.start_link(
  transport: :http,
  url: "http://localhost:8080",
  endpoint: "/mcp/v1",
  
  # Authentication
  headers: [
    {"Authorization", "Bearer #{token}"},
    {"X-API-Key", api_key}
  ],
  
  # Connection options
  timeout: 30_000,
  request_timeout: 15_000,
  
  # SSE options
  use_sse: true,
  sse_timeout: 120_000
)

Native BEAM Transport

For high-performance intra-cluster communication:

# Service configuration using macro
defmodule MyToolService do
  use ExMCP.Service, name: :my_tools
  
  # Service automatically registers with ExMCP.Native
  @impl true
  def handle_mcp_request(method, params, state) do
    # Handle MCP requests
    {:ok, response, state}
  end
end

# Start service
{:ok, _} = MyToolService.start_link()

# Direct calls
{:ok, result} = ExMCP.Native.call(:my_tools, "list_tools", %{})

# Cross-node calls
{:ok, result} = ExMCP.Native.call(
  {:my_service, :"node@cluster.local"}, 
  "tools/call", 
  %{"name" => "calculator", "arguments" => %{"a" => 1, "b" => 2}}
)

# For distributed clusters, opt in to Horde adapter:
# config :ex_mcp, :service_registry, ExMCP.ServiceRegistry.Horde

Security Configuration

Authentication Methods

Bearer Token Authentication

# Client with bearer token
{:ok, client} = ExMCP.Client.start_link(
  transport: :http,
  url: "https://api.example.com",
  security: %{
    auth: {:bearer, "your-jwt-token"}
  }
)

API Key Authentication

# Client with API key
{:ok, client} = ExMCP.Client.start_link(
  transport: :http,
  url: "https://api.example.com",
  headers: [{"X-API-Key", "your-api-key"}]
)

Basic Authentication

# Client with basic auth
{:ok, client} = ExMCP.Client.start_link(
  transport: :http,
  url: "https://api.example.com",
  security: %{
    auth: {:basic, "username", "password"}
  }
)

OAuth 2.1 Configuration

# OAuth 2.1 Resource Server (MCP 2025-06-18)
config :ex_mcp, :oauth,
  issuer: "https://auth.example.com",
  audience: "mcp-api",
  jwks_uri: "https://auth.example.com/.well-known/jwks.json",
  cache_jwks: true,
  cache_ttl: 3600

JWT Client Authentication (private_key_jwt)

For machine-to-machine auth using JWT client assertions (RFC 7523) instead of client secrets:

# Load your private key
{:ok, private_key} = ExMCP.Authorization.JWT.load_key({:pem_file, "/path/to/key.pem"})

# Client credentials flow with JWT authentication
{:ok, token} = ExMCP.Authorization.OAuthFlow.client_credentials_jwt_flow(%{
  client_id: "my-client",
  private_key: private_key,
  token_endpoint: "https://auth.example.com/token",
  scopes: ["mcp:read", "mcp:write"]
})

Enterprise-Managed Authorization (ID-JAG)

For enterprise SSO via ID-JAG (RFC 8693 token exchange + RFC 7523 JWT bearer grant):

# After obtaining an OIDC ID token from the enterprise IdP
{:ok, access_token} = ExMCP.Authorization.EnterpriseFlow.execute(%{
  id_token: id_token_from_oidc,
  idp_token_endpoint: "https://idp.enterprise.com/token",
  as_issuer: "https://auth.example.com",
  resource_url: "https://mcp.example.com",
  client_id: "my-client"
})

TLS/SSL Configuration

# Server with TLS
{:ok, server} = ExMCP.Server.start_link(
  handler: MyHandler,
  transport: :http,
  port: 8443,
  
  # TLS options
  https: [
    port: 8443,
    keyfile: "/path/to/private.key",
    certfile: "/path/to/certificate.crt",
    cacertfile: "/path/to/ca-bundle.crt"
  ]
)

# Client with TLS verification
{:ok, client} = ExMCP.Client.start_link(
  transport: :http,
  url: "https://secure.example.com:8443",
  
  # TLS options
  ssl: [
    verify: :verify_peer,
    cacertfile: "/path/to/ca-bundle.crt",
    depth: 2
  ]
)

CORS Configuration

# Server with CORS
{:ok, server} = ExMCP.Server.start_link(
  handler: MyHandler,
  transport: :http,
  
  # CORS settings
  cors_enabled: true,
  allowed_origins: [
    "https://app.example.com",
    "https://staging.example.com"
  ],
  allowed_methods: ["GET", "POST", "OPTIONS"],
  allowed_headers: ["Authorization", "Content-Type"],
  max_age: 3600
)

Origin Validation

# Client with origin validation
{:ok, client} = ExMCP.Client.start_link(
  transport: :http,
  url: "https://api.example.com",
  security: %{
    validate_origin: true,
    allowed_origins: ["https://app.example.com"]
  }
)

Logging Configuration

Global Logging

# Set global ExMCP log level
ExMCP.Logging.set_global_level("debug")

# Available levels: "debug", "info", "warning", "error"

Production Logger Configuration

ExMCP includes security audit logging with structured metadata. Configure appropriately for your environment:

Development Configuration

# config/dev.exs
config :logger, :console,
  metadata: [:request_id, :tag, :audit, :client_id, :reason, :registration_type]

Production Configuration Options

Option 1: JSON Structured Logging (Recommended)

# In mix.exs
{:logger_json, "~> 5.1"}

# In config/prod.exs
config :logger,
  backends: [LoggerJSON]

config :logger_json, :backend,
  metadata: :all,  # Captures ALL metadata automatically
  json_encoder: Jason,
  formatter: LoggerJSON.Formatters.GoogleCloudLogger

Option 2: Separate Security Audit Logs

# config/prod.exs
config :logger,
  backends: [:console, {LoggerFileBackend, :security_audit}]

config :logger, :security_audit,
  path: "/var/log/ex_mcp/security_audit.log",
  level: :info,
  format: "$date $time [$level] $metadata $message\n",
  metadata: [:tag, :audit, :client_id, :reason, :registration_type],
  metadata_filter: [tag: :security_audit]  # Only security logs

Option 3: External Log Aggregation

# For ELK Stack, Datadog, etc.
{:logstash_logger_backend, "~> 3.0"}

config :logger,
  backends: [{LogstashLoggerBackend, :logstash}]

config :logger, :logstash,
  host: "logstash.example.com",
  port: 5514,
  metadata: :all,
  type: "ex_mcp_security"

Logger Metadata Fields

ExMCP uses these metadata fields for security and debugging:

  • :tag - Log categorization (e.g., :security_audit, :client_registration)
  • :audit - Detailed audit log entries with timestamps and actions
  • :client_id - Client identification for request tracking
  • :reason - Failure reasons and error details
  • :registration_type - Client registration type (:static or :dynamic)
  • :request_id - Correlation ID for distributed tracing

Structured Logging in Code

# Server logging with automatic security sanitization
defmodule MyServer do
  use ExMCP.Server.Handler

  @impl true
  def handle_call_tool("login", params, state) do
    server = self()
    
    # This logs to both MCP clients AND Elixir Logger
    # Sensitive data is automatically sanitized
    ExMCP.Logging.info(server, "User login attempt", %{
      username: params["username"],
      password: params["password"],  # Will be sanitized to "***"
      timestamp: DateTime.utc_now()
    })
    
    # ... handle login logic
    {:ok, result, state}
  end
end

# All RFC 5424 log levels supported
ExMCP.Logging.debug(server, "Debug info", %{details: "..."})
ExMCP.Logging.info(server, "Operation completed") 
ExMCP.Logging.warning(server, "Deprecated feature used")
ExMCP.Logging.error(server, "Operation failed", %{error: "connection_timeout"})
ExMCP.Logging.critical(server, "System component failure")

Performance Configuration

Connection Pooling

# HTTP client with connection pooling
{:ok, client} = ExMCP.Client.start_link(
  transport: :http,
  url: "https://api.example.com",
  
  # Connection pool settings
  pool_size: 10,
  pool_max_overflow: 5,
  pool_timeout: 5000,
  
  # Keep-alive settings
  recv_timeout: 30_000,
  keepalive_timeout: 60_000
)

Native BEAM Optimization

# Default: local Registry (zero deps, single-node)
# config :ex_mcp, :service_registry, ExMCP.ServiceRegistry.Local

# For distributed clusters, use Horde adapter:
# config :ex_mcp, :service_registry, ExMCP.ServiceRegistry.Horde

Progress Tracking Configuration

# Configure progress tracking thresholds
config :ex_mcp, :progress,
  # Minimum interval between progress notifications (ms)
  min_interval: 100,
  
  # Maximum number of pending progress tokens
  max_pending: 1000,
  
  # Cleanup interval for expired tokens (ms)
  cleanup_interval: 30_000

Production Configuration

Complete Production Example

# config/prod.exs
import Config

# ExMCP main configuration
config :ex_mcp,
  protocol_version: "2025-11-25",
  default_timeout: 30_000,
  request_timeout: 15_000,
  auto_reconnect: true,
  reconnect_backoff: [initial: 1000, max: 30_000],
  log_level: :info

# Security configuration
config :ex_mcp, :security,
  enforce_https: true,
  validate_origins: true,
  max_request_size: 10_485_760  # 10MB

# OAuth 2.1 configuration (if using)
config :ex_mcp, :oauth,
  issuer: {:system, "OAUTH_ISSUER"},
  audience: {:system, "OAUTH_AUDIENCE"},
  jwks_uri: {:system, "OAUTH_JWKS_URI"},
  cache_jwks: true,
  cache_ttl: 3600

# Logging for production
config :logger,
  backends: [LoggerJSON]

config :logger_json, :backend,
  metadata: :all,
  json_encoder: Jason,
  formatter: LoggerJSON.Formatters.GoogleCloudLogger

# Native BEAM registry (default: local, use Horde for distributed)
# config :ex_mcp, :service_registry, ExMCP.ServiceRegistry.Horde

# SSL/TLS configuration
config :ex_mcp, :ssl,
  verify: :verify_peer,
  depth: 2,
  versions: [:"tlsv1.2", :"tlsv1.3"],
  ciphers: [
    "ECDHE-RSA-AES256-GCM-SHA384",
    "ECDHE-RSA-AES128-GCM-SHA256"
  ]

Environment Variables

Support for runtime configuration via environment variables:

# config/runtime.exs
import Config

if config_env() == :prod do
  config :ex_mcp,
    protocol_version: System.get_env("MCP_PROTOCOL_VERSION", "2025-11-25"),
    default_timeout: String.to_integer(System.get_env("MCP_TIMEOUT", "30000")),
    log_level: String.to_atom(System.get_env("MCP_LOG_LEVEL", "info"))

  # OAuth configuration from environment
  if oauth_issuer = System.get_env("OAUTH_ISSUER") do
    config :ex_mcp, :oauth,
      issuer: oauth_issuer,
      audience: System.get_env("OAUTH_AUDIENCE"),
      jwks_uri: System.get_env("OAUTH_JWKS_URI")
  end
end

Environment-Specific Configuration

Development Configuration

# config/dev.exs
import Config

config :ex_mcp,
  protocol_version: "2025-11-25",
  log_level: :debug,
  auto_reconnect: false  # Easier debugging

# Verbose logging for development
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id, :tag, :audit, :client_id]

Test Configuration

# config/test.exs
import Config

config :ex_mcp,
  protocol_version: "2025-11-25",
  default_timeout: 5_000,  # Faster tests
  log_level: :warning,     # Reduce test noise
  auto_reconnect: false    # Predictable test behavior

# Test logging
config :logger,
  level: :warning,
  backends: []  # Disable logging in tests

Staging Configuration

# config/staging.exs
import Config

# Similar to production but with more lenient settings
config :ex_mcp,
  protocol_version: "2025-11-25",
  log_level: :debug,               # More verbose for debugging
  
  # Relaxed security for testing
  security: %{
    enforce_https: false,
    validate_origins: false
  }

Configuration Validation

Runtime Validation

# Add to application startup
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    # Validate ExMCP configuration on startup
    :ok = ExMCP.Config.validate!()
    
    children = [
      # ... your children
    ]
    
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Configuration Helpers

# Check current configuration
ExMCP.Config.get(:protocol_version)
ExMCP.Config.get(:default_timeout)

# Validate specific settings
ExMCP.Config.validate_protocol_version("2025-11-25")
ExMCP.Config.validate_transport_config(:http, options)

Troubleshooting Configuration

Common Issues

  1. Protocol Version Mismatch

    Error: Unsupported protocol version "invalid"
    Solution: Use "2024-11-05", "2025-03-26", "2025-06-18", or "2025-11-25"
  2. Transport Configuration

    Error: Invalid transport options
    Solution: Check transport-specific configuration requirements
  3. Authentication Failures

    Error: Authentication failed
    Solution: Verify tokens, certificates, and authentication method

Configuration Testing

# Test configuration in IEx
iex> ExMCP.Config.test_config()
%{
  protocol_version: "2025-11-25",
  transports: [:stdio, :http, :native],
  security: %{...},
  valid: true
}

Debug Configuration

Enable debug logging to troubleshoot configuration issues:

# Temporarily enable debug logging
ExMCP.Logging.set_global_level("debug")

# Check what configuration is actually loaded
ExMCP.Config.dump()

For more configuration help, see: