chaoticingenuity / laravel-mcp-server
Model Context Protocol (MCP) server implementation for Laravel
Requires
- php: ^8.1
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0
Suggests
- silber/bouncer: ^1.0 - Enhanced role and ability management for MCP permissions
This package is auto-updated.
Last update: 2025-09-23 03:30:54 UTC
README
A Laravel package that implements the Model Context Protocol (MCP) server specification, enabling AI models to interact with your Laravel application through a standardized protocol.
Features
- ✅ Full MCP 2024-11-05 protocol implementation
- ✅ Tool and Resource support with JSON-RPC 2.0
- ✅ Fine-grained permission system with field-level access control
- ✅ Template resources with parameter extraction
- ✅ Multiple authentication methods (API keys, Basic Auth, Bearer tokens)
- ✅ Flexible custom authentication system with database integration
- ✅ Comprehensive security middleware stack
- ✅ Rate limiting with per-client controls and burst protection
- ✅ Performance monitoring and detailed logging
- ✅ Extensible architecture for custom tools/resources
- ✅ Laravel 10+, 11+ and 12+ support
- ✅ Comprehensive test suite
- ✅ Auto-discovery of tools and resources
- ✅ NEW: Advanced permission resolution system with custom resolvers
- ✅ NEW: Enhanced field-level security with data filtering
- ✅ NEW: API key audit logging and usage tracking
- ✅ NEW: Per-key rate limiting with burst protection
- ✅ NEW: Security enhancements (key rotation, scope-based permissions)
- ✅ NEW: Performance monitoring and metrics tracking
Installation
Install the package via Composer:
composer require chaoticingenuity/laravel-mcp-server
Quick Start
1. Publish Configuration
php artisan vendor:publish --tag=mcp-config
2. Publish Controllers and Middleware
php artisan vendor:publish --tag=mcp-controllers
3. Laravel Version-Specific Setup
Laravel 11+ (including Laravel 12) Setup (Recommended Method)
For Laravel 11+, manually register middleware in bootstrap/app.php
:
<?php use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // Register MCP middleware aliases $middleware->alias([ 'mcp.auth' => \App\Http\Middleware\MCPAuthMiddleware::class, 'mcp.logging' => \App\Http\Middleware\MCPLoggingMiddleware::class, 'mcp.security' => \App\Http\Middleware\MCPSecurityMiddleware::class, 'mcp.throttle' => \App\Http\Middleware\MCPThrottleMiddleware::class, ]); }) ->withExceptions(function (Exceptions $exceptions) { // })->create();
Add the MCP route to your routes/api.php
:
<?php use App\Http\Controllers\MCPController; use Illuminate\Support\Facades\Route; Route::post('/mcp', [MCPController::class, 'handle']) ->middleware([ 'mcp.security', 'mcp.auth', 'mcp.throttle', 'mcp.logging' ]) ->name('mcp.handle');
Laravel 10 Setup
For Laravel 10, middleware is automatically registered by the service provider. Just ensure your routes/api.php
includes the MCP route (same as above).
4. Set Environment Variables
Add to your .env
file:
# Server Information MCP_SERVER_NAME="Your MCP Server" MCP_SERVER_VERSION="1.0.0" # Authentication - Static Keys (Optional) MCP_API_KEY_1=mcp_live_sk_1234567890abcdef1234567890abcdef MCP_CLIENT_1=gpt_client # User Model Configuration (for database authentication) MCP_USER_MODEL=App\Models\User MCP_USER_FOREIGN_KEY=user_id MCP_USER_OWNER_KEY=id # Security MCP_REQUIRE_HTTPS=true MCP_ALLOWED_IPS=192.168.1.0/24 # Rate Limiting MCP_RATE_LIMIT=100 MCP_BURST_LIMIT=20 # Logging & Monitoring MCP_LOG_REQUESTS=true MCP_LOG_PERFORMANCE=true MCP_LOG_API_USAGE=false MCP_TRACK_PERMISSIONS=false # Performance MCP_AUTH_CACHE_DURATION=300 MCP_CACHE_PERMISSION_RESULTS=true MCP_PERMISSION_CACHE_TTL=300
5. Configure Authentication
Choose your authentication strategy:
Option A: Static Configuration (Simple)
Edit config/mcp.php
:
'clients' => [ 'gpt_client' => [ 'permissions' => [ 'tools.echo', 'resources.status', ], 'field_access' => [], 'metadata' => ['tier' => 'standard'] ], ],
Option B: Database Authentication (Recommended)
- Add MCP support to your User model:
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use ChaoticIngenuity\LaravelMCP\Traits\HasMCPAuthentication; use ChaoticIngenuity\LaravelMCP\Contracts\MCPUserInterface; class User extends Authenticatable implements MCPUserInterface { use HasMCPAuthentication; protected $fillable = [ 'name', 'email', 'password', 'mcp_enabled', 'mcp_permissions', 'mcp_field_access' ]; protected $casts = [ 'email_verified_at' => 'datetime', 'password' => 'hashed', 'mcp_enabled' => 'boolean', 'mcp_permissions' => 'array', 'mcp_field_access' => 'array', 'mcp_tokens' => 'array', 'mcp_scopes' => 'array', // NEW: Scope-based permissions ]; // Optional: Customize permissions based on user roles public function getMCPPermissions(): array { if ($this->hasRole('admin')) { return ['admin']; } if ($this->hasRole('api_user')) { return [ 'tools.*', 'resources.catalog', 'products.read' ]; } return parent::getMCPPermissions(); } }
- Run migrations:
# Option A: For API key-based authentication (includes both api_keys table and users table fields) php artisan vendor:publish --tag=mcp-migrations-api-keys php artisan migrate # Option B: For Bouncer integration (enables MCP for existing Bouncer users) php artisan vendor:publish --tag=mcp-migrations-bouncer php artisan migrate # Option C: Custom implementation (no migrations needed - implement custom storage in your User model) # See examples in documentation for user table columns, JSON storage, etc.
// In the migration file public function up(): void { Schema::table('users', function (Blueprint $table) { $table->boolean('mcp_enabled')->default(false); $table->json('mcp_permissions')->nullable(); $table->json('mcp_field_access')->nullable(); $table->json('mcp_tokens')->nullable(); $table->json('mcp_scopes')->nullable(); // NEW: Scope-based permissions $table->index('mcp_enabled'); }); }
- Enable custom database authenticator:
// config/mcp.php 'auth' => [ 'custom_authenticators' => [ \App\Services\Custom\MCP\Auth\DatabaseAuthenticator::class, ], ],
6. Test the Installation
# Generate an API key for a user (if using database auth) php artisan tinker >>> $user = User::find(1); >>> $apiKey = $user->generateMCPApiKey('test_client', ['tools.*']); >>> echo $apiKey->key; # Test the endpoint curl -X POST http://your-app.com/api/mcp \ -H "Content-Type: application/json" \ -H "X-MCP-API-Key: mcp_live_sk_1234567890abcdef1234567890abcdef" \ -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
7. Verification
Check that everything is working:
# Verify routes are registered php artisan route:list --name=mcp # Check middleware is loaded php artisan route:show mcp.handle # Clear cache if needed php artisan optimize:clear
Advanced Features (v1.1.0+)
Permission Resolution System
The package now includes a flexible permission resolution system that allows custom permission logic:
// Create custom permission resolvers class OrganizationPermissionResolver implements PermissionResolverInterface { public function resolveUserPermissions($user): array { // Custom logic based on organization membership $orgPermissions = $user->organization->mcp_permissions ?? []; $userPermissions = $user->mcp_permissions ?? []; return array_merge($orgPermissions, $userPermissions); } public function resolveUserFieldAccess($user): array { // Dynamic field access based on user tier return match($user->subscription_tier) { 'premium' => ['*'], 'standard' => ['name', 'price', 'description'], 'basic' => ['name', 'price'], default => [] }; } public function canResolve($user): bool { return $user->organization !== null; } public function getPriority(): int { return 50; // Medium priority } } // Register in config/mcp.php 'auth' => [ 'custom_permission_resolvers' => [ \App\Services\MCP\OrganizationPermissionResolver::class, ], ],
Enhanced API Key Management
Advanced API key features with audit logging and per-key rate limiting:
// Generate API key with specific rate limits and scopes $apiKey = $user->generateMCPApiKey('mobile_app', ['tools.search'], 'Mobile App Key'); // Set per-key rate limits $apiKey->update([ 'rate_limit_per_minute' => 500, 'rate_limit_burst' => 50 ]); // Track usage automatically $keyWithTracking = $user->getMCPApiKeyWithTracking($keyValue); // Rotate keys securely $newKey = $user->rotateMCPApiKey($oldKeyValue); // Get detailed usage analytics $analytics = $user->getMCPApiKeysSummary(); // Returns: total_keys, active_keys, expired_keys, usage_stats
Scope-Based Permissions
Implement fine-grained access control with scopes:
// Add scopes to users $user->addMCPScope('read:products'); $user->addMCPScope('write:orders'); // Check scopes in tools/resources public function isAccessibleTo(ContextInterface $context): bool { return $context->hasScope('read:products') || $context->hasPermission('admin'); } // Configure scope-based client permissions 'clients' => [ 'mobile_app' => [ 'permissions' => ['tools.*'], 'scopes' => ['read:products', 'read:categories'], 'metadata' => ['app_version' => '2.1.0'] ] ]
Data Filtering & Field Security
Automatically filter response data based on field access permissions:
public function execute(array $arguments, ContextInterface $context): ResultInterface { $products = Product::all()->toArray(); // Automatically filter fields based on user permissions $filteredProducts = array_map(function($product) use ($context) { return $context->filterFields('product', $product); }, $products); return Result::success([ 'products' => $filteredProducts, 'accessible_fields' => $context->getAccessibleFields('product') ]); }
Performance Monitoring & Analytics
Built-in performance tracking and usage analytics:
// Enable in .env MCP_TRACK_PERMISSIONS=true MCP_LOG_API_USAGE=true // Access metrics via Cache $permissionStats = Cache::get('mcp.perms.tools.search_products.granted', 0); $deniedAttempts = Cache::get('mcp.perms.tools.search_products.denied', 0); // View API usage logs tail -f storage/logs/mcp.log | grep "API Key Usage"
Authentication Methods
The package supports multiple authentication strategies that can be used simultaneously:
Static Configuration
Configure static API keys, basic auth, and bearer tokens:
// config/mcp.php 'auth' => [ 'api_keys' => [ env('MCP_API_KEY_1'), env('MCP_API_KEY_2'), ], 'basic_auth' => [ env('MCP_BASIC_USER_1') => env('MCP_BASIC_PASS_1'), ], 'bearer_tokens' => [ env('MCP_BEARER_TOKEN_1'), ], ],
Database Authentication
Store API keys in your database with user relationships:
// Generate API keys for users $user = User::find(1); $apiKey = $user->generateMCPApiKey('mobile_app', ['tools.search', 'resources.catalog']); // Check if a key is valid $isValid = $user->isMCPApiKeyValid($apiKey->key); // Revoke a key $user->revokeMCPApiKey($apiKey->key); // Get key information (without exposing the actual key) $keyInfo = $user->getMCPApiKeyInfo($apiKey->key);
Custom Authentication
Create your own authentication logic:
// app/Services/Custom/MCP/Auth/CustomAuthenticator.php class CustomAuthenticator implements AuthenticatorInterface { public function handles(string $type): bool { return $type === 'custom_token'; } public function authenticate(string $type, array $credentials): AuthenticationResult { // Your custom authentication logic $token = $credentials['token'] ?? ''; if ($this->isValidCustomToken($token)) { return AuthenticationResult::success('custom_client_id'); } return AuthenticationResult::failure('Invalid custom token'); } } // Register in config/mcp.php 'auth' => [ 'custom_authenticators' => [ \App\Services\Custom\MCP\Auth\CustomAuthenticator::class, ], ],
Flexible Key Storage Patterns
The package supports multiple API key storage strategies:
Separate Table (Default)
Uses the ApiKey
model with foreign key relationships:
// Uses the HasMCPAuthentication trait defaults class User extends Authenticatable implements MCPUserInterface { use HasMCPAuthentication; // No overrides needed - uses ApiKey model }
User Table Columns
Store hashed keys directly in user table columns:
class User extends Authenticatable implements MCPUserInterface { use HasMCPAuthentication; protected $fillable = [ 'mcp_api_key_1', 'mcp_api_key_2', 'mcp_api_key_1_meta', 'mcp_api_key_2_meta' ]; protected $casts = [ 'mcp_api_key_1_meta' => 'array', 'mcp_api_key_2_meta' => 'array', ]; protected $hidden = [ 'mcp_api_key_1', 'mcp_api_key_2' ]; public function generateMCPApiKey(string $clientIdentifier, array $scopes = [], ?string $name = null): ApiKey { $key = 'mcp_' . bin2hex(random_bytes(32)); $hashedKey = Hash::make($key); if (empty($this->mcp_api_key_1)) { $this->update([ 'mcp_api_key_1' => $hashedKey, 'mcp_api_key_1_meta' => [ 'client_identifier' => $clientIdentifier, 'scopes' => $scopes, 'created_at' => now(), ], ]); } // Return mock ApiKey for compatibility } public function isMCPApiKeyValid(string $key): bool { return ($this->mcp_api_key_1 && Hash::check($key, $this->mcp_api_key_1)) || ($this->mcp_api_key_2 && Hash::check($key, $this->mcp_api_key_2)); } }
JSON Column Storage
Store all keys in a single JSON column:
class User extends Authenticatable implements MCPUserInterface { use HasMCPAuthentication; protected $casts = [ 'mcp_api_keys' => 'array', ]; public function isMCPApiKeyValid(string $key): bool { $keys = $this->mcp_api_keys ?? []; foreach ($keys as $keyData) { if (Hash::check($key, $keyData['hash']) && ($keyData['is_active'] ?? false) && (!isset($keyData['expires_at']) || now()->isBefore($keyData['expires_at']))) { return true; } } return false; } }
Client Permissions
Configure fine-grained permissions for each client:
'clients' => [ 'admin_client' => [ 'permissions' => ['admin'], // Full access 'field_access' => ['*'], 'metadata' => ['tier' => 'admin'] ], 'api_client' => [ 'permissions' => [ 'tools.*', // All tools 'resources.*', // All resources 'products.read', // Entity-level permission ], 'field_access' => [ 'user' => ['name', 'email'], // Limited fields 'product' => ['*'], // All product fields ], 'rate_limit' => [ 'requests_per_minute' => 1000, // Client-specific limits 'burst_limit' => 50, ], 'metadata' => ['tier' => 'premium'] ], 'public_client' => [ 'permissions' => [ 'tools.echo', 'resources.status', ], 'field_access' => [ 'product' => ['name', 'price'], // Public fields only ], 'metadata' => ['tier' => 'public'] ] ]
Creating Custom Tools
1. Generate Tool Stub
php artisan vendor:publish --tag=mcp-stubs
2. Create Your Tool
<?php namespace App\Services\Custom\MCP\Tools; use ChaoticIngenuity\LaravelMCP\Contracts\{ToolInterface, ContextInterface, ResultInterface}; use ChaoticIngenuity\LaravelMCP\Core\Result; class SearchProductsTool implements ToolInterface { public function getName(): string { return 'search_products'; } public function getDescription(): string { return 'Search products in the database with advanced filtering'; } public function getInputSchema(): array { return [ 'type' => 'object', 'properties' => [ 'filters' => [ 'type' => 'array', 'description' => 'Search filters to apply', 'items' => [ 'type' => 'object', 'properties' => [ 'field' => [ 'type' => 'string', 'description' => 'Field name to filter on' ], 'operator' => [ 'type' => 'string', 'enum' => ['=', '!=', '>', '<', '>=', '<=', 'like', 'in'], 'description' => 'Comparison operator' ], 'value' => [ 'oneOf' => [ {'type' => 'string'}, {'type' => 'number'}, {'type' => 'array'} ], 'description' => 'Value to compare against' ] ], 'required' => ['field', 'operator', 'value'] ] ], 'limit' => [ 'type' => 'integer', 'default' => 10, 'maximum' => 100, 'description' => 'Maximum number of results' ], 'sort' => [ 'type' => 'object', 'properties' => [ 'field' => ['type' => 'string'], 'direction' => ['type' => 'string', 'enum' => ['asc', 'desc']] ] ] ], 'required' => ['filters'], 'examples' => [ { 'description' => 'Find electronics under $500', 'filters' => [ {'field' => 'category', 'operator' => '=', 'value' => 'electronics'}, {'field' => 'price', 'operator' => '<', 'value' => 500} ], 'limit' => 20, 'sort' => {'field' => 'name', 'direction' => 'asc'} } ] ]; } public function isAccessibleTo(ContextInterface $context): bool { return $context->hasPermission('tools.search_products') || $context->hasPermission('admin'); } public function execute(array $arguments, ContextInterface $context): ResultInterface { $filters = $arguments['filters'] ?? []; $limit = min($arguments['limit'] ?? 10, 100); $sort = $arguments['sort'] ?? ['field' => 'created_at', 'direction' => 'desc']; // Apply field-level access control $accessibleFields = $context->getAccessibleFields('product'); $query = Product::query(); // Apply filters with field access checking foreach ($filters as $filter) { $field = $filter['field']; $operator = $filter['operator']; $value = $filter['value']; // Skip inaccessible fields if (!$context->hasFieldAccess('product', $field)) { continue; } match($operator) { '=' => $query->where($field, $value), '!=' => $query->where($field, '!=', $value), '>', '<', '>=', '<=' => $query->where($field, $operator, $value), 'like' => $query->where($field, 'like', "%{$value}%"), 'in' => $query->whereIn($field, (array) $value), default => null }; } // Apply sorting if field is accessible if ($context->hasFieldAccess('product', $sort['field'])) { $query->orderBy($sort['field'], $sort['direction']); } $products = $query->limit($limit)->get(); // Filter response fields based on permissions $filteredProducts = $products->map(function ($product) use ($accessibleFields) { if (in_array('*', $accessibleFields)) { return $product->toArray(); } return $product->only($accessibleFields); }); return Result::success([ 'products' => $filteredProducts, 'total' => $filteredProducts->count(), 'applied_filters' => $filters, 'accessible_fields' => $accessibleFields ], [ 'execution_time_ms' => microtime(true) * 1000, 'client_id' => $context->getClientId() ]); } }
3. Register Your Tool
Add to config/mcp.php
:
'custom' => [ 'tools' => [ \App\Services\Custom\MCP\Tools\SearchProductsTool::class, ], ]
Creating Custom Resources
1. Static Resources
<?php namespace App\Services\Custom\MCP\Resources; use ChaoticIngenuity\LaravelMCP\Contracts\{ResourceInterface, ContextInterface, ResultInterface}; use ChaoticIngenuity\LaravelMCP\Core\Result; class ProductCatalogResource implements ResourceInterface { public function getUri(): string { return 'catalog://products'; } public function getName(): string { return 'Product Catalog'; } public function getDescription(): string { return 'Complete product catalog with categories and metadata'; } public function getMimeType(): string { return 'application/json'; } public function isTemplate(): bool { return false; } public function isAccessibleTo(ContextInterface $context): bool { return $context->hasPermission('resources.catalog') || $context->hasPermission('products.read'); } public function getAccessibleFields(ContextInterface $context): array { return $context->getAccessibleFields('product'); } public function getContent(string $uri, ContextInterface $context): ResultInterface { $accessibleFields = $this->getAccessibleFields($context); // Cache based on client permissions $cacheKey = 'mcp.catalog.products.' . md5(json_encode($accessibleFields)); $data = Cache::remember($cacheKey, 300, function () use ($accessibleFields) { $query = Product::with('categories'); if (!in_array('*', $accessibleFields)) { $query->select($accessibleFields); } return [ 'products' => $query->get()->toArray(), 'categories' => Category::all(['id', 'name', 'description'])->toArray(), ]; }); return Result::success([ ...$data, 'metadata' => [ 'total_products' => count($data['products']), 'total_categories' => count($data['categories']), 'accessible_fields' => $accessibleFields, 'last_updated' => now()->toISOString(), ] ], [ 'cached' => true, 'cache_key' => $cacheKey ]); } }
2. Template Resources
Resources that accept parameters in the URI:
class ProductResource implements ResourceInterface { public function getUri(): string { return 'product://{product_id}'; } public function isTemplate(): bool { return true; } public function getContent(string $uri, ContextInterface $context): ResultInterface { // Extract product_id from URI: product://123 $productId = str_replace('product://', '', $uri); if (!$context->hasFieldAccess('product', 'id')) { return Result::error('Access denied to product data'); } $accessibleFields = $context->getAccessibleFields('product'); $product = Product::find($productId); if (!$product) { return Result::error('Product not found'); } $filteredProduct = in_array('*', $accessibleFields) ? $product->toArray() : $product->only($accessibleFields); return Result::success($filteredProduct, [ 'product_id' => $productId, 'accessible_fields' => $accessibleFields ]); } }
3. Register Your Resources
// config/mcp.php 'custom' => [ 'resources' => [ \App\Services\Custom\MCP\Resources\ProductCatalogResource::class, \App\Services\Custom\MCP\Resources\ProductResource::class, ], ]
Security
Field-Level Access Control
Control which fields clients can access at a granular level:
'clients' => [ 'public_api' => [ 'field_access' => [ 'user' => ['name', 'avatar'], // Public fields only 'product' => ['name', 'price', 'description'], // No internal data 'order' => [], // No access to orders ] ], 'partner_api' => [ 'field_access' => [ 'user' => ['name', 'email', 'phone'], // More fields for partners 'product' => ['*'], // All product data 'order' => ['id', 'status', 'total'], // Limited order access ] ], 'internal_admin' => [ 'permissions' => ['admin'], // Full access 'field_access' => ['*'] // All fields, all entities ] ]
Middleware Stack
The package includes comprehensive security middleware:
- MCPSecurityMiddleware: HTTPS enforcement, IP whitelisting, security headers
- MCPAuthMiddleware: Multi-method authentication with custom authenticators
- MCPThrottleMiddleware: Rate limiting with per-client and burst controls
- MCPLoggingMiddleware: Request/response logging with performance metrics
Environment-Based Security
# Production Security Settings MCP_REQUIRE_HTTPS=true MCP_ALLOWED_IPS=203.0.113.0/24,192.168.1.0/24 MCP_BLOCK_SUSPICIOUS_UA=true # Never enable these in production MCP_DEBUG_EXPOSE_SYSTEM_INFO=false MCP_DEBUG_DETAILED_ERRORS=false MCP_DEBUG_LOG_REQUESTS=false
IP Whitelisting
Support for both individual IPs and CIDR notation:
# Individual IPs MCP_ALLOWED_IPS=203.0.113.1,203.0.113.2 # CIDR ranges MCP_ALLOWED_IPS=192.168.1.0/24,10.0.0.0/8 # Mixed MCP_ALLOWED_IPS=203.0.113.1,192.168.1.0/24,10.0.0.0/8
Rate Limiting
Configure rate limits globally and per-client:
# Global Defaults MCP_RATE_LIMIT=60 # Requests per minute MCP_BURST_LIMIT=10 # Short-term burst allowance # Per-client limits in config/mcp.php 'clients' => [ 'high_volume_client' => [ 'rate_limit' => [ 'requests_per_minute' => 1000, 'burst_limit' => 100, ], ], ]
What's New in v1.0.0 🎉
Enhanced Permission Management
- 🔗 Optional Bouncer Integration: Seamlessly integrate with Laravel Bouncer for advanced role-based permissions
- 🔄 Permission Manager Architecture: Pluggable permission system with automatic fallback
- ⚡ Performance Optimizations: Registry template matching with compiled pattern caching
- 🛡️ Enhanced Security: Improved authentication flow and circular dependency prevention
New Features
- 🚀 MCP Setup Command:
php artisan mcp:setup --bouncer
for easy configuration - 📊 Comprehensive Test Suite: 74 tests covering all major functionality
- 🎛️ Flexible Authentication: Multiple storage patterns for API keys (database, user columns, JSON)
- 📝 Better Documentation: Enhanced examples, troubleshooting guides, and migration instructions
Developer Experience
- ✨ PSR-12 Compliance: Full code formatting standards with .editorconfig
- 🔧 Auto-Detection: Automatic Bouncer package detection and configuration
- 📚 Rich Examples: Comprehensive examples for both basic and Bouncer usage
- 🐛 Improved Error Handling: Better validation and error messages
Quick Setup with Bouncer (Optional)
If you want enhanced permission management with Laravel Bouncer:
# Install Bouncer (optional) composer require silber/bouncer # Setup MCP with Bouncer integration php artisan mcp:setup --bouncer # Enable Bouncer in your .env MCP_BOUNCER_ENABLED=true
Testing
Comprehensive Test Suite (v1.0.0)
The package includes 74 comprehensive tests covering:
- ✅ Core MCP Protocol: Initialize, tools/list, tools/call, resources/*
- ✅ Authentication: API keys, Basic auth, Bearer tokens, custom authenticators
- ✅ Permission Management: Default and Bouncer permission managers
- ✅ Bouncer Integration: Package detection, fallback behavior, configuration
- ✅ Registry Performance: Template matching, caching, memory optimization
- ✅ Security: Access control, rate limiting, validation
- ✅ HTTP Integration: Middleware, routing, error handling
Running Tests
# Run all tests composer test # Run specific test suites vendor/bin/phpunit tests/Feature/ vendor/bin/phpunit tests/Unit/ # Run with coverage composer test-coverage # Individual test files vendor/bin/phpunit tests/Feature/MCPServerTest.php vendor/bin/phpunit tests/Feature/AuthenticationTest.php vendor/bin/phpunit tests/Unit/PermissionManagerTest.php vendor/bin/phpunit tests/Feature/BouncerIntegrationTest.php vendor/bin/phpunit tests/Unit/RegistryTest.php
Performance Benchmarks
The v1.0.0 test suite includes performance validation:
- Template URI Matching: 1000 matches complete in <100ms
- Memory Usage: 1000 tool/resource registrations use <5MB
- Authentication: Cached permission lookups for optimal performance
- Registry: Compiled pattern caching for repeated template matches
Running Package Tests
composer test
Testing Your Implementation
# List available tools curl -X POST http://localhost/api/mcp \ -H "Content-Type: application/json" \ -H "X-MCP-API-Key: your_key" \ -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' # Execute a tool with structured query curl -X POST http://localhost/api/mcp \ -H "Content-Type: application/json" \ -H "X-MCP-API-Key: your_key" \ -d '{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "search_products", "arguments": { "filters": [ {"field": "category", "operator": "=", "value": "electronics"}, {"field": "price", "operator": "<", "value": 500} ], "limit": 5, "sort": {"field": "name", "direction": "asc"} } }, "id": 2 }' # Read a static resource curl -X POST http://localhost/api/mcp \ -H "Content-Type: application/json" \ -H "X-MCP-API-Key: your_key" \ -d '{ "jsonrpc": "2.0", "method": "resources/read", "params": {"uri": "catalog://products"}, "id": 3 }' # Read a template resource curl -X POST http://localhost/api/mcp \ -H "Content-Type: application/json" \ -H "X-MCP-API-Key: your_key" \ -d '{ "jsonrpc": "2.0", "method": "resources/read", "params": {"uri": "product://123"}, "id": 4 }' # Test with database authentication curl -X POST http://localhost/api/mcp \ -H "Content-Type: application/json" \ -H "X-User-Token: user_token_xyz" \ -H "X-User-ID: 123" \ -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 5}'
MCP Protocol Methods
The package implements all standard MCP methods:
Method | Description | Response |
---|---|---|
initialize |
Initialize MCP session | Server capabilities and info |
tools/list |
List available tools | Array of tool definitions |
tools/call |
Execute a tool | Tool execution result |
resources/list |
List available static resources | Array of resource definitions |
resources/read |
Read resource content | Resource content |
resources/templates/list |
List template resources | Array of template definitions |
Error Handling
The package provides standardized JSON-RPC 2.0 error responses:
Code | Meaning | When Used |
---|---|---|
-32001 |
Authentication required | Invalid/missing credentials |
-32002 |
Access denied | Insufficient permissions |
-32003 |
Rate limit exceeded | Too many requests |
-32602 |
Invalid params | Missing/invalid parameters |
-32603 |
Internal error | Server-side errors |
Exception Types
The package uses specific exception types for better error handling:
MCPAuthenticationException
: Thrown for authentication failures, invalid client IDs, or authorization issues- Client ID Validation: Client IDs must be alphanumeric with dots, hyphens, or underscores only (max 255 chars)
Input Validation
Client IDs are automatically validated and must meet these requirements:
- Non-empty
- Maximum 255 characters
- Only alphanumeric characters, dots (.), hyphens (-), and underscores (_)
- Invalid characters will throw
MCPAuthenticationException
Example error response:
{ "jsonrpc": "2.0", "error": { "code": -32002, "message": "Access denied", "data": { "required_permission": "tools.search_products", "client_permissions": ["tools.echo", "resources.status"] } }, "id": 1 }
Performance Considerations
Caching Strategies
Implement intelligent caching in your custom tools and resources:
public function getContent(string $uri, ContextInterface $context): ResultInterface { // Cache based on URI and client permissions $cacheKey = "mcp.resource.{$uri}." . md5(json_encode([ $context->getClientId(), $context->getAccessibleFields('product') ])); $data = Cache::remember($cacheKey, 300, function() use ($uri, $context) { return $this->fetchExpensiveData($uri, $context); }); return Result::success($data, ['cached' => true]); }
Database Optimization
- Use appropriate indexes for your search fields
- Implement field selection based on access permissions
- Consider read replicas for heavy MCP usage
- Use
select()
to limit returned columns
// Optimize queries based on field access $accessibleFields = $context->getAccessibleFields('product'); $query = Product::query(); if (!in_array('*', $accessibleFields)) { $query->select($accessibleFields); }
Authentication Performance
- Database authenticators use intelligent caching
- Cache duration is configurable per environment
- Failed authentication attempts are rate limited
- Async updates (last_used_at) don't block requests
// config/mcp.php 'auth' => [ 'cache_duration' => 300, // 5 minutes 'rate_limit_failed_attempts' => 10, // Per IP per hour ],
Monitoring
Use the built-in logging to monitor performance:
# View MCP request logs tail -f storage/logs/mcp.log # Monitor slow requests (>500ms) grep "duration_ms.*[5-9][0-9][0-9]" storage/logs/mcp.log # Check authentication patterns grep "auth_type" storage/logs/mcp.log | sort | uniq -c # Monitor rate limiting grep "rate.*limit" storage/logs/mcp.log
User Management
Creating API Keys for Users
// Generate API keys programmatically $user = User::find(1); // Basic API key $apiKey = $user->generateMCPApiKey('mobile_app_v1'); // API key with specific scopes $apiKey = $user->generateMCPApiKey('analytics_dashboard', [ 'tools.search_products', 'resources.catalog', 'products.read' ], 'Analytics Dashboard Key'); // Check key validity $isValid = $user->isMCPApiKeyValid($apiKey->key); // Get key information (safe - no actual key exposed) $keyInfo = $user->getMCPApiKeyInfo($apiKey->key); // Get summary of all user's keys $summary = $user->getMCPApiKeysSummary();
User Permission Management
// Enable MCP access for a user $user->update(['mcp_enabled' => true]); // Set user permissions $user->update([ 'mcp_permissions' => [ 'tools.search_products', 'tools.get_inventory', 'resources.catalog' ] ]); // Set field access $user->update([ 'mcp_field_access' => [ 'product' => ['name', 'price', 'category'], 'user' => ['name', 'email'] ] ]); // Check permissions $hasAccess = $user->hasMCPPermission('tools.search_products'); $hasFieldAccess = $user->hasMCPFieldAccess('product', 'price');
Bulk Operations
// Revoke all keys for a user $revokedCount = $user->revokeAllMCPApiKeys(); // Clean up expired keys $cleanedCount = $user->cleanupExpiredMCPApiKeys(); // Enable MCP for multiple users User::whereIn('id', [1, 2, 3])->update(['mcp_enabled' => true]); // Find users with specific permissions $apiUsers = User::withMCPAccess() ->whereJsonContains('mcp_permissions', 'tools.search_products') ->get();
Deployment
Production Checklist
- Set
MCP_DEBUG_*
variables tofalse
- Use strong API keys (32+ characters)
- Configure rate limits appropriate for your infrastructure
- Enable HTTPS (
MCP_REQUIRE_HTTPS=true
) - Set up IP whitelisting if applicable
- Configure proper logging levels
- Set up log rotation for MCP logs
- Test all middleware is properly registered
- Verify error responses don't expose sensitive information
- Set up monitoring for MCP endpoints
- Configure database indexes for API key lookups
- Test authentication performance under load
Environment Variables
# Production Settings APP_ENV=production MCP_REQUIRE_HTTPS=true MCP_DEBUG_EXPOSE_SYSTEM_INFO=false MCP_DEBUG_DETAILED_ERRORS=false # Security MCP_ALLOWED_IPS=your.trusted.networks MCP_BLOCK_SUSPICIOUS_UA=true # Performance MCP_RATE_LIMIT=1000 MCP_BURST_LIMIT=50 MCP_LOG_PERFORMANCE=true MCP_AUTH_CACHE_DURATION=300 # Monitoring MCP_LOG_REQUESTS=true MCP_LOG_LEVEL=warning MCP_LOG_RETENTION_DAYS=30 # Database Authentication MCP_USER_MODEL=App\Models\User MCP_USER_FOREIGN_KEY=user_id MCP_USER_OWNER_KEY=id
Docker
# Add to your Dockerfile COPY config/mcp.php /var/www/config/ RUN php artisan config:cache # Ensure logs directory is writable RUN mkdir -p /var/www/storage/logs && \ chown -R www-data:www-data /var/www/storage # Add database indexes for performance COPY database/migrations/add_mcp_indexes.php /var/www/database/migrations/
Database Indexes
Create proper indexes for optimal performance:
// database/migrations/add_mcp_indexes.php public function up(): void { Schema::table('api_keys', function (Blueprint $table) { $table->index(['key', 'is_active']); $table->index(['user_id', 'is_active']); $table->index(['client_identifier']); $table->index(['expires_at']); }); Schema::table('users', function (Blueprint $table) { $table->index('mcp_enabled'); }); }
Load Balancing
MCP servers are stateless and can be load balanced normally. Consider:
- Session affinity not required
- Rate limiting may need shared storage (Redis)
- Resource caching benefits from shared cache
- Log aggregation for monitoring across instances
- Database connection pooling for authentication
Monitoring Setup
# Set up log monitoring tail -f storage/logs/mcp.log | grep ERROR # Monitor authentication failures grep "Authentication.*failed" storage/logs/mcp.log # Track performance metrics grep "duration_ms" storage/logs/mcp.log | awk '{print $6}' | sort -n # Monitor rate limiting grep "rate.*limit.*exceeded" storage/logs/mcp.log
Troubleshooting
Common Issues
Middleware Not Found Error
# Laravel 11 - Check bootstrap/app.php has middleware aliases grep -A 10 "withMiddleware" bootstrap/app.php # Laravel 10 - Ensure service provider is registered php artisan config:show app.providers # Verify route middleware php artisan route:show mcp.handle
Tool/Resource Not Found
# Check registration in config/mcp.php php artisan config:show mcp.custom # Verify class exists and implements correct interface php artisan tinker >>> class_exists(\App\Services\Custom\MCP\Tools\YourTool::class) >>> class_implements(\App\Services\Custom\MCP\Tools\YourTool::class) # Clear application cache php artisan optimize:clear
Database Authentication Issues
# Check user model configuration php artisan config:show mcp.auth.user_model # Test user has MCP access php artisan tinker >>> $user = User::find(1) >>> $user->hasMCPAccess() >>> $user->getMCPPermissions() # Check API key generation >>> $key = $user->generateMCPApiKey('test') >>> $user->isMCPApiKeyValid($key->key)
Client ID Validation Errors
# MCPAuthenticationException: Invalid client ID format # Client IDs must be alphanumeric with dots, hyphens, or underscores only # Valid client IDs: # ✅ "mobile_app" # ✅ "api-client" # ✅ "client.v1" # ❌ "client@domain" # ❌ "client/path" # ❌ "client with spaces" # Test client ID validation php artisan tinker >>> app(\ChaoticIngenuity\LaravelMCP\Core\ContextFactory::class)->createFromClient('test-client')
Field Access Issues
# Field access permissions are now working correctly (v1.0.0 bug fix) # Test field access in tinker php artisan tinker >>> $user = User::find(1) >>> $user->hasMCPFieldAccess('product', 'price') >>> $user->getMCPFieldAccess()
Access Denied Errors
# Check client permissions in config/mcp.php php artisan config:show mcp.auth.clients # Verify field-level access configuration php artisan tinker >>> $context = app(\ChaoticIngenuity\LaravelMCP\Core\ContextFactory::class)->createFromClient('your_client') >>> $context->hasPermission('tools.your_tool') >>> $context->hasFieldAccess('product', 'price') # Check authentication headers are correct curl -v -H "X-MCP-API-Key: your_key" http://localhost/api/mcp
Rate Limit Issues
# Check current rate limit settings php artisan config:show mcp.rate_limit # Verify client identification is working grep "mcp_client" storage/logs/mcp.log # Clear rate limit cache if needed php artisan cache:forget "mcp_throttle:your_client" # Check for burst limit hits grep "burst.*limit" storage/logs/mcp.log
Debug Mode
Enable debug mode for development:
MCP_DEBUG_DETAILED_ERRORS=true MCP_DEBUG_LOG_REQUESTS=true MCP_LOG_LEVEL=debug MCP_DEBUG_EXPOSE_SYSTEM_INFO=true
Check logs for detailed information:
# View all MCP logs tail -f storage/logs/mcp.log # Filter by specific client grep "client_1" storage/logs/mcp.log # Find slow requests (>1 second) grep "duration_ms.*[1-9][0-9][0-9][0-9]" storage/logs/mcp.log # Check authentication flow grep -E "(Authentication|auth_type)" storage/logs/mcp.log # Monitor memory usage grep "memory_mb" storage/logs/mcp.log | sort -k6 -n
Performance Debugging
Monitor and optimize performance:
# Average request duration grep "duration_ms" storage/logs/mcp.log | \ awk '{print $6}' | awk '{sum+=$1; count++} END {print sum/count}' # Memory usage patterns grep "memory_mb" storage/logs/mcp.log | \ awk '{print $8}' | sort -n | tail -10 # Most used tools/resources grep -E "(tool|resource)" storage/logs/mcp.log | \ awk '{print $4}' | sort | uniq -c | sort -nr # Authentication method distribution grep "auth_type" storage/logs/mcp.log | \ awk '{print $7}' | sort | uniq -c
Testing Authentication
// Test database authentication in tinker php artisan tinker // Create a test user and API key >>> $user = User::first() >>> $user->update(['mcp_enabled' => true]) >>> $key = $user->generateMCPApiKey('test_client', ['tools.*']) >>> echo $key->key // Test key validation >>> $user->isMCPApiKeyValid($key->key) >>> $user->getMCPApiKeyInfo($key->key) // Test custom authenticator >>> $auth = app(\App\Services\Custom\MCP\Auth\DatabaseAuthenticator::class) >>> $result = $auth->authenticate('api_key', ['api_key' => $key->key]) >>> $result->isSuccess() >>> $result->getClientId()
Laravel Version Compatibility
Laravel 11
- ✅ Fully supported
- ⚠️ Requires manual middleware registration in
bootstrap/app.php
- ✅ Uses new application structure
- ✅ Follows Laravel 11 conventions
Laravel 10
- ✅ Fully supported
- ✅ Automatic middleware registration
- ✅ Traditional
app/Http/Kernel.php
structure
Migration from Laravel 10 to 11
If upgrading your Laravel application:
- Update middleware registration in
bootstrap/app.php
- Remove any manual middleware registration from
app/Http/Kernel.php
- Test all MCP endpoints still work
- Update any custom middleware following Laravel 11 patterns
Advanced Configuration
Custom Context Factory
Create custom client context logic:
// app/Services/Custom/MCP/CustomContextFactory.php class CustomContextFactory extends ContextFactory { public function createFromClient(string $clientId): ContextInterface { // Custom logic for determining client permissions if (str_starts_with($clientId, 'user_')) { $userId = str_replace('user_', '', $clientId); $user = User::find($userId); return new Context( clientId: $clientId, permissions: $user->getMCPPermissions(), fieldAccess: $user->getMCPFieldAccess(), metadata: ['user_id' => $userId, 'tier' => $user->subscription_tier] ); } return parent::createFromClient($clientId); } } // Register in service provider $this->app->singleton(ContextFactory::class, CustomContextFactory::class);
Multi-Tenant Support
Configure MCP for multi-tenant applications:
class TenantAwareMCPMiddleware { public function handle(Request $request, Closure $next) { $tenant = $this->resolveTenant($request); if (!$tenant) { return response()->json([ 'jsonrpc' => '2.0', 'error' => ['code' => -32001, 'message' => 'Invalid tenant'] ], 401); } // Set tenant context $request->merge(['mcp_tenant' => $tenant->id]); return $next($request); } }
Custom Schema Validation
Add JSON Schema validation for tool inputs:
use JsonSchema\Validator; public function execute(array $arguments, ContextInterface $context): ResultInterface { // Validate against schema $validator = new Validator(); $validator->validate($arguments, $this->getInputSchema()); if (!$validator->isValid()) { $errors = array_map(fn($error) => $error['message'], $validator->getErrors()); return Result::error('Validation failed: ' . implode(', ', $errors)); } // Continue with execution... }
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Add tests for new functionality
- Ensure all tests pass (
composer test
) - Follow PSR-12 coding standards (
composer format
) - Update documentation as needed
- Submit a pull request
Development Setup
# Clone the repository git clone https://github.com/chaoticingenuity/laravel-mcp-server.git cd laravel-mcp-server # Install dependencies composer install # Run tests composer test # Run code formatting composer format # Run static analysis composer analyse
Package Structure
src/
├── Contracts/ # Interfaces and contracts
├── Core/ # Core functionality (Registry, Context, etc.)
├── Auth/ # Authentication system
├── Http/ # Controllers and middleware
├── Tools/ # Built-in tools
├── Resources/ # Built-in resources
├── Traits/ # Reusable traits
└── Providers/ # Service providers
config/ # Configuration files
routes/ # Route definitions
tests/ # Test suite
examples/ # Usage examples
stubs/ # Code generation stubs
License
MIT License - see LICENSE file for details.
Changelog
See CHANGELOG.md for version history.
Support
- Documentation: GitHub Wiki
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Security Issues: Email security@chaoticingenuity.com
Credits
- Model Context Protocol Specification
- Laravel Framework
- JSON-RPC 2.0 Specification
- All contributors who have helped improve this package
Related Packages
- Laravel Sanctum: For API token authentication
- Laravel Passport: For OAuth2 authentication
- Laravel Telescope: For debugging and monitoring
- Laravel Horizon: For queue monitoring
- Spatie Laravel Permission: For role-based permissions