Laravel MCP SDK - Complete Integration Guide
The Laravel MCP SDK provides comprehensive support for the MCP 2025-06-18 specification with Laravel's elegant developer experience. This guide covers server implementation, client usage, OpenAI integration for agentic workflows, and production best practices.
Quick Start
Installation
bash
composer require dalehurley/laravel-php-mcp-sdk
Basic Setup
bash
# Install MCP scaffolding
php artisan mcp:install
# Create your first tool
php artisan make:mcp-tool WeatherTool
# Start the server
php artisan mcp:server start
Service Provider Setup
Basic Service Provider
Create app/Providers/McpServiceProvider.php
:
php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use MCP\Server\McpServer;
use MCP\Types\Implementation;
class McpServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(McpServer::class, function ($app) {
$server = new McpServer(
new Implementation(
config('app.name', 'laravel-app'),
config('app.version', '1.0.0'),
'Laravel MCP Server'
)
);
$this->registerLaravelTools($server);
$this->registerLaravelResources($server);
return $server;
});
}
public function boot(): void
{
$this->publishes([
__DIR__.'/../../config/mcp.php' => config_path('mcp.php'),
], 'mcp-config');
}
private function registerLaravelTools(McpServer $server): void
{
// User management tools
$server->tool(
'get_users',
'Get users from database',
[
'type' => 'object',
'properties' => [
'limit' => ['type' => 'integer', 'default' => 10],
'search' => ['type' => 'string']
]
],
function (array $params): array {
$query = \App\Models\User::query();
if (!empty($params['search'])) {
$query->where('name', 'like', "%{$params['search']}%")
->orWhere('email', 'like', "%{$params['search']}%");
}
$users = $query->limit($params['limit'] ?? 10)->get();
return [
'content' => [[
'type' => 'text',
'text' => $users->toJson(JSON_PRETTY_PRINT)
]]
];
}
);
// Artisan command execution
$server->tool(
'run_artisan',
'Execute Artisan command',
[
'type' => 'object',
'properties' => [
'command' => ['type' => 'string'],
'arguments' => ['type' => 'array', 'default' => []]
],
'required' => ['command']
],
function (array $params): array {
$command = $params['command'];
$arguments = $params['arguments'] ?? [];
// Security: Only allow specific commands
$allowedCommands = [
'cache:clear', 'config:cache', 'route:list',
'queue:work', 'migrate:status'
];
if (!in_array($command, $allowedCommands)) {
throw new \MCP\Types\McpError(
\MCP\Types\ErrorCode::Forbidden,
"Command '{$command}' not allowed"
);
}
$exitCode = \Illuminate\Support\Facades\Artisan::call($command, $arguments);
$output = \Illuminate\Support\Facades\Artisan::output();
return [
'content' => [[
'type' => 'text',
'text' => "Command: {$command}\nExit Code: {$exitCode}\nOutput:\n{$output}"
]]
];
}
);
// Cache management
$server->tool(
'cache_get',
'Get cached value',
[
'type' => 'object',
'properties' => [
'key' => ['type' => 'string']
],
'required' => ['key']
],
function (array $params): array {
$value = \Illuminate\Support\Facades\Cache::get($params['key']);
return [
'content' => [[
'type' => 'text',
'text' => json_encode([
'key' => $params['key'],
'value' => $value,
'exists' => $value !== null
], JSON_PRETTY_PRINT)
]]
];
}
);
$server->tool(
'cache_set',
'Set cached value',
[
'type' => 'object',
'properties' => [
'key' => ['type' => 'string'],
'value' => ['type' => 'string'],
'ttl' => ['type' => 'integer', 'default' => 3600]
],
'required' => ['key', 'value']
],
function (array $params): array {
$ttl = $params['ttl'] ?? 3600;
\Illuminate\Support\Facades\Cache::put(
$params['key'],
$params['value'],
$ttl
);
return [
'content' => [[
'type' => 'text',
'text' => "Cached '{$params['key']}' for {$ttl} seconds"
]]
];
}
);
}
private function registerLaravelResources(McpServer $server): void
{
// Application configuration
$server->resource(
'app-config',
'laravel://config/{key}',
'application/json',
function (string $uri): array {
if (!preg_match('/laravel:\/\/config\/(.+)/', $uri, $matches)) {
throw new \MCP\Types\McpError(
\MCP\Types\ErrorCode::InvalidParams,
'Invalid config URI format'
);
}
$configKey = urldecode($matches[1]);
$value = config($configKey);
return [
'contents' => [[
'uri' => $uri,
'mimeType' => 'application/json',
'text' => json_encode([
'key' => $configKey,
'value' => $value
], JSON_PRETTY_PRINT)
]]
];
}
);
// Route information
$server->resource(
'routes',
'laravel://routes',
'application/json',
function (string $uri): array {
$routes = collect(\Illuminate\Support\Facades\Route::getRoutes())
->map(function ($route) {
return [
'uri' => $route->uri(),
'methods' => $route->methods(),
'name' => $route->getName(),
'action' => $route->getActionName()
];
})
->values()
->all();
return [
'contents' => [[
'uri' => $uri,
'mimeType' => 'application/json',
'text' => json_encode($routes, JSON_PRETTY_PRINT)
]]
];
}
);
// Application logs
$server->resource(
'logs',
'laravel://logs/{date}',
'text/plain',
function (string $uri): array {
if (!preg_match('/laravel:\/\/logs\/(.+)/', $uri, $matches)) {
throw new \MCP\Types\McpError(
\MCP\Types\ErrorCode::InvalidParams,
'Invalid log URI format'
);
}
$date = urldecode($matches[1]);
$logFile = storage_path("logs/laravel-{$date}.log");
if (!file_exists($logFile)) {
throw new \MCP\Types\McpError(
\MCP\Types\ErrorCode::InvalidParams,
"Log file not found for date: {$date}"
);
}
$content = file_get_contents($logFile);
return [
'contents' => [[
'uri' => $uri,
'mimeType' => 'text/plain',
'text' => $content
]]
];
}
);
}
}
Register the Service Provider
Add to config/app.php
:
php
'providers' => [
// Other providers...
App\Providers\McpServiceProvider::class,
],
Configuration
MCP Configuration File
Create config/mcp.php
:
php
<?php
return [
'server' => [
'name' => env('MCP_SERVER_NAME', config('app.name')),
'version' => env('MCP_SERVER_VERSION', '1.0.0'),
'description' => env('MCP_SERVER_DESCRIPTION', 'Laravel MCP Server'),
],
'transport' => [
'default' => env('MCP_TRANSPORT', 'stdio'),
'stdio' => [
'buffer_size' => 8192,
],
'http' => [
'host' => env('MCP_HTTP_HOST', '127.0.0.1'),
'port' => (int) env('MCP_HTTP_PORT', 3000),
'ssl' => env('MCP_HTTP_SSL', false),
'cors' => [
'enabled' => true,
'origins' => explode(',', env('MCP_CORS_ORIGINS', '*')),
'methods' => ['GET', 'POST'],
'headers' => ['Content-Type', 'Authorization'],
],
],
],
'auth' => [
'enabled' => env('MCP_AUTH_ENABLED', false),
'provider' => env('MCP_AUTH_PROVIDER', 'sanctum'),
'sanctum' => [
'guard' => 'sanctum',
],
'oauth2' => [
'client_id' => env('MCP_OAUTH_CLIENT_ID'),
'client_secret' => env('MCP_OAUTH_CLIENT_SECRET'),
'redirect_uri' => env('MCP_OAUTH_REDIRECT_URI'),
'scopes' => explode(',', env('MCP_OAUTH_SCOPES', 'read,write')),
],
],
'tools' => [
'enabled' => [
'user_management' => true,
'cache_management' => true,
'artisan_commands' => true,
'database_queries' => false, // Security: disabled by default
],
'artisan' => [
'allowed_commands' => [
'cache:clear',
'config:cache',
'route:list',
'queue:work',
'migrate:status'
],
],
],
'resources' => [
'enabled' => [
'config' => true,
'routes' => true,
'logs' => false, // Security: disabled by default
],
],
];
Artisan Commands
MCP Server Command
Create app/Console/Commands/McpServerCommand.php
:
php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use MCP\Server\McpServer;
use MCP\Server\Transport\StdioServerTransport;
use MCP\Server\Transport\HttpServerTransport;
class McpServerCommand extends Command
{
protected $signature = 'mcp:serve
{--transport=stdio : Transport type (stdio, http)}
{--host=127.0.0.1 : HTTP host}
{--port=3000 : HTTP port}';
protected $description = 'Start the MCP server';
public function handle(): int
{
$server = app(McpServer::class);
$transport = $this->option('transport');
$this->info("Starting MCP server with {$transport} transport...");
try {
if ($transport === 'http') {
$httpTransport = new HttpServerTransport([
'host' => $this->option('host'),
'port' => (int) $this->option('port'),
]);
$this->info("HTTP server starting on {$this->option('host')}:{$this->option('port')}");
$server->connect($httpTransport)->await();
} else {
$stdioTransport = new StdioServerTransport();
$server->connect($stdioTransport)->await();
}
return 0;
} catch (\Exception $e) {
$this->error("Failed to start MCP server: {$e->getMessage()}");
return 1;
}
}
}
MCP Client Command
Create app/Console/Commands/McpClientCommand.php
:
php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use MCP\Client\Client;
use MCP\Client\Transport\StdioClientTransport;
use MCP\Types\Implementation;
class McpClientCommand extends Command
{
protected $signature = 'mcp:client
{server : Path to MCP server}
{--tool= : Tool to call}
{--params= : JSON parameters for tool}';
protected $description = 'Connect to MCP server and call tools';
public function handle(): int
{
$serverPath = $this->argument('server');
$toolName = $this->option('tool');
$params = $this->option('params') ? json_decode($this->option('params'), true) : [];
$client = new Client(new Implementation('laravel-client', '1.0.0'));
$transport = new StdioClientTransport([
'command' => 'php',
'args' => [$serverPath]
]);
try {
$this->info("Connecting to MCP server: {$serverPath}");
$client->connect($transport)->await();
if ($toolName) {
$this->info("Calling tool: {$toolName}");
$result = $client->callTool($toolName, $params)->await();
$this->line($result['content'][0]['text']);
} else {
// List available capabilities
$tools = $client->listTools()->await();
$this->info("Available tools:");
foreach ($tools['tools'] as $tool) {
$this->line(" - {$tool['name']}: {$tool['description']}");
}
}
$client->close()->await();
return 0;
} catch (\Exception $e) {
$this->error("MCP client error: {$e->getMessage()}");
return 1;
}
}
}
Controller Integration
MCP Controller
Create app/Http/Controllers/McpController.php
:
php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use MCP\Server\McpServer;
use MCP\Client\Client;
use MCP\Client\Transport\StdioClientTransport;
use MCP\Types\Implementation;
class McpController extends Controller
{
private McpServer $server;
public function __construct(McpServer $server)
{
$this->server = $server;
}
public function listTools(): JsonResponse
{
$tools = $this->server->listTools();
return response()->json([
'tools' => $tools['tools'],
'count' => count($tools['tools'])
]);
}
public function callTool(Request $request): JsonResponse
{
$request->validate([
'tool_name' => 'required|string',
'parameters' => 'array'
]);
try {
$result = $this->server->callToolByName(
$request->input('tool_name'),
$request->input('parameters', [])
);
return response()->json(['result' => $result]);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage()
], 400);
}
}
public function connectToExternalServer(Request $request): JsonResponse
{
$request->validate([
'server_command' => 'required|string',
'server_args' => 'array'
]);
$client = new Client(new Implementation('laravel-client', '1.0.0'));
$transport = new StdioClientTransport([
'command' => $request->input('server_command'),
'args' => $request->input('server_args', [])
]);
try {
$client->connect($transport)->await();
$tools = $client->listTools()->await();
$client->close()->await();
return response()->json([
'connected' => true,
'tools' => $tools['tools']
]);
} catch (\Exception $e) {
return response()->json([
'connected' => false,
'error' => $e->getMessage()
], 400);
}
}
}
Middleware Integration
MCP Authentication Middleware
Create app/Http/Middleware/McpAuthMiddleware.php
:
php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use MCP\Types\McpError;
use MCP\Types\ErrorCode;
class McpAuthMiddleware
{
public function handle(Request $request, Closure $next)
{
// Skip authentication for public endpoints
if ($request->is('mcp/public/*')) {
return $next($request);
}
$token = $request->bearerToken();
if (!$token) {
throw new McpError(
ErrorCode::Unauthorized,
'Authentication token required'
);
}
// Validate token using Laravel Sanctum
$user = \Laravel\Sanctum\PersonalAccessToken::findToken($token)?->tokenable;
if (!$user) {
throw new McpError(
ErrorCode::Unauthorized,
'Invalid authentication token'
);
}
// Add user to request
$request->merge(['mcp_user' => $user]);
return $next($request);
}
}
Blade Components
MCP Client Component
Create resources/views/components/mcp-client.blade.php
:
php
<div x-data="mcpClient()" class="mcp-client">
<div class="mb-4">
<h3 class="text-lg font-semibold">MCP Server Connection</h3>
<div class="flex gap-2">
<input x-model="serverPath" placeholder="Server path" class="border rounded px-3 py-1">
<button @click="connect()" :disabled="connecting" class="bg-blue-500 text-white px-4 py-1 rounded">
<span x-show="!connecting">Connect</span>
<span x-show="connecting">Connecting...</span>
</button>
</div>
</div>
<div x-show="connected" class="space-y-4">
<div>
<h4 class="font-medium">Available Tools</h4>
<ul class="list-disc list-inside">
<template x-for="tool in tools">
<li>
<button @click="selectTool(tool)" class="text-blue-600 hover:underline" x-text="tool.name"></button>
- <span x-text="tool.description"></span>
</li>
</template>
</ul>
</div>
<div x-show="selectedTool">
<h4 class="font-medium">Call Tool: <span x-text="selectedTool?.name"></span></h4>
<textarea x-model="toolParams" placeholder="Parameters (JSON)" class="w-full border rounded p-2"></textarea>
<button @click="callTool()" class="bg-green-500 text-white px-4 py-1 rounded mt-2">
Call Tool
</button>
</div>
<div x-show="result">
<h4 class="font-medium">Result</h4>
<pre class="bg-gray-100 p-3 rounded overflow-auto" x-text="result"></pre>
</div>
</div>
</div>
<script>
function mcpClient() {
return {
serverPath: '',
connecting: false,
connected: false,
tools: [],
selectedTool: null,
toolParams: '{}',
result: '',
async connect() {
this.connecting = true;
try {
const response = await fetch('/mcp/connect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
server_command: 'php',
server_args: [this.serverPath]
})
});
const data = await response.json();
if (data.connected) {
this.connected = true;
this.tools = data.tools;
} else {
alert('Connection failed: ' + data.error);
}
} catch (error) {
alert('Connection error: ' + error.message);
} finally {
this.connecting = false;
}
},
selectTool(tool) {
this.selectedTool = tool;
this.result = '';
},
async callTool() {
try {
const params = JSON.parse(this.toolParams);
const response = await fetch('/mcp/call-tool', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
tool_name: this.selectedTool.name,
parameters: params
})
});
const data = await response.json();
if (data.result) {
this.result = JSON.stringify(data.result, null, 2);
} else {
this.result = 'Error: ' + data.error;
}
} catch (error) {
this.result = 'Error: ' + error.message;
}
}
}
}
</script>
Routes
MCP Routes
Create routes/mcp.php
:
php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\McpController;
Route::prefix('mcp')->group(function () {
// Public routes
Route::get('/tools', [McpController::class, 'listTools']);
Route::get('/resources', [McpController::class, 'listResources']);
// Protected routes
Route::middleware(['auth:sanctum'])->group(function () {
Route::post('/call-tool', [McpController::class, 'callTool']);
Route::post('/connect', [McpController::class, 'connectToExternalServer']);
Route::get('/server-info', [McpController::class, 'getServerInfo']);
});
});
Queue Integration
MCP Job
Create app/Jobs/McpToolCallJob.php
:
php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use MCP\Client\Client;
use MCP\Client\Transport\StdioClientTransport;
use MCP\Types\Implementation;
class McpToolCallJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private string $serverPath,
private string $toolName,
private array $parameters,
private ?string $callbackUrl = null
) {}
public function handle(): void
{
$client = new Client(new Implementation('laravel-queue-client', '1.0.0'));
$transport = new StdioClientTransport([
'command' => 'php',
'args' => [$this->serverPath]
]);
try {
$client->connect($transport)->await();
$result = $client->callTool($this->toolName, $this->parameters)->await();
$client->close()->await();
// Store result or send callback
if ($this->callbackUrl) {
$this->sendCallback($result);
} else {
$this->storeResult($result);
}
} catch (\Exception $e) {
$this->fail($e);
}
}
private function sendCallback(array $result): void
{
// Send HTTP callback with result
\Illuminate\Support\Facades\Http::post($this->callbackUrl, [
'tool' => $this->toolName,
'result' => $result,
'status' => 'completed'
]);
}
private function storeResult(array $result): void
{
// Store result in database or cache
\Illuminate\Support\Facades\Cache::put(
"mcp_result_{$this->job->uuid}",
$result,
3600
);
}
}
Testing
Feature Tests
Create tests/Feature/McpIntegrationTest.php
:
php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use MCP\Server\McpServer;
use MCP\Types\Implementation;
class McpIntegrationTest extends TestCase
{
public function test_mcp_server_creation(): void
{
$server = app(McpServer::class);
$this->assertInstanceOf(McpServer::class, $server);
}
public function test_mcp_tools_endpoint(): void
{
$response = $this->get('/mcp/tools');
$response->assertStatus(200)
->assertJsonStructure([
'tools',
'count'
]);
}
public function test_mcp_tool_call(): void
{
$this->actingAs($this->createUser());
$response = $this->postJson('/mcp/call-tool', [
'tool_name' => 'cache_get',
'parameters' => ['key' => 'test-key']
]);
$response->assertStatus(200)
->assertJsonStructure(['result']);
}
private function createUser()
{
return \App\Models\User::factory()->create();
}
}
Environment Configuration
.env Settings
env
# MCP Configuration
MCP_SERVER_NAME="My Laravel App"
MCP_SERVER_VERSION="1.0.0"
MCP_TRANSPORT=stdio
MCP_HTTP_HOST=127.0.0.1
MCP_HTTP_PORT=3000
MCP_AUTH_ENABLED=true
MCP_AUTH_PROVIDER=sanctum
# CORS Settings
MCP_CORS_ORIGINS="http://localhost:3000,https://myapp.com"
# OAuth Settings (if using OAuth)
MCP_OAUTH_CLIENT_ID=your-client-id
MCP_OAUTH_CLIENT_SECRET=your-client-secret
MCP_OAUTH_REDIRECT_URI=http://localhost:8000/auth/callback
Usage Examples
Basic Server Usage
bash
# Start MCP server with STDIO transport
php artisan mcp:serve
# Start HTTP server
php artisan mcp:serve --transport=http --port=3000
# Connect to external MCP server
php artisan mcp:client path/to/server.php --tool=get_weather --params='{"city":"London"}'
Programmatic Usage
php
// In a Laravel controller or service
class WeatherService
{
public function getWeather(string $city): array
{
$client = new Client(new Implementation('weather-client', '1.0.0'));
$transport = new StdioClientTransport([
'command' => 'php',
'args' => [base_path('weather-server.php')]
]);
$client->connect($transport)->await();
$result = $client->callTool('get_weather', ['city' => $city])->await();
$client->close()->await();
return $result;
}
}
Queue Integration
php
// Dispatch MCP tool call to queue
McpToolCallJob::dispatch(
'path/to/server.php',
'process_data',
['data' => $largeDataset],
'https://myapp.com/mcp/callback'
);
Best Practices
1. Security
- Use Laravel Sanctum for API authentication
- Validate all MCP tool parameters
- Restrict allowed Artisan commands
- Implement proper authorization checks
2. Performance
- Use Laravel's queue system for long-running MCP operations
- Implement caching for frequently called tools
- Use connection pooling for multiple MCP servers
- Monitor performance with Laravel Telescope
3. Error Handling
- Use Laravel's exception handling for MCP errors
- Log all MCP operations for debugging
- Implement proper fallback mechanisms
- Use Laravel's retry mechanisms for transient failures
4. Testing
- Write feature tests for MCP endpoints
- Mock MCP servers for unit testing
- Use Laravel's HTTP testing for integration tests
- Test both success and error scenarios
Deployment
Production Configuration
php
// config/mcp.php - Production settings
return [
'server' => [
'name' => env('APP_NAME'),
'version' => env('APP_VERSION', '1.0.0'),
],
'transport' => [
'default' => 'http',
'http' => [
'host' => '0.0.0.0',
'port' => (int) env('MCP_PORT', 3000),
'ssl' => true,
],
],
'auth' => [
'enabled' => true,
'provider' => 'sanctum',
],
];
Docker Integration
dockerfile
FROM php:8.1-fpm
# Install dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Copy application
COPY . /var/www
WORKDIR /var/www
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Set permissions
RUN chown -R www-data:www-data /var/www
# Start MCP server
CMD ["php", "artisan", "mcp:serve", "--transport=http", "--host=0.0.0.0"]