Overview
Chatbot Platform is built on Laravel MCP , making it easy to create custom MCP servers that expose tools to AI models. This guide walks you through building your own MCP server from scratch.
Prerequisites
Laravel 12+ application
laravel/mcp package installed
Basic understanding of Laravel controllers and services
Familiarity with JSON schemas
Installation
Laravel MCP is already included in Chatbot Platform. For standalone Laravel applications:
composer require laravel/mcp
Project Structure
app/Mcp/Servers/
└── YourServer/
├── YourServerServer.php # Server definition
├── YourService.php # Business logic (optional)
└── Tools/
├── ToolOne.php
└── ToolTwo.php
Creating a Server
Step 1: Generate the Server Class
php artisan make:mcp-server WeatherServer
This creates app/Mcp/Servers/WeatherServer/WeatherServer.php:
<? php
namespace App\Mcp\Servers\WeatherServer ;
use Laravel\Mcp\ Server ;
class WeatherServer extends Server
{
/**
* The MCP server's name.
*/
protected string $name = 'Weather' ;
/**
* The MCP server's version.
*/
protected string $version = '1.0.0' ;
/**
* The MCP server's instructions for the LLM.
*/
protected string $instructions = <<<' MARKDOWN '
This MCP server provides weather information for AI assistants.
## Authentication
Requires an API key from WeatherAPI.com. Include in the request header:
X-Weather-Api-Key: YOUR_API_KEY
## Available Tools
### get-weather
Get current weather conditions for a location.
### get-forecast
Get multi-day weather forecast.
MARKDOWN;
/**
* The tools registered with this MCP server.
*/
protected array $tools = [
// Tool classes will be added here
];
protected array $resources = [];
protected array $prompts = [];
}
Generate a tool:
php artisan make:mcp-tool GetWeather --server=WeatherServer
This creates app/Mcp/Servers/WeatherServer/Tools/GetWeather.php:
<? php
namespace App\Mcp\Servers\WeatherServer\Tools ;
use Illuminate\Contracts\JsonSchema\ JsonSchema ;
use Laravel\Mcp\Server\ Tool ;
class GetWeather extends Tool
{
/**
* The tool's name.
*/
protected string $name = 'get-weather' ;
/**
* The tool's description for the LLM.
*/
protected string $description = 'Get current weather conditions for a location. Provide a city name, postal code, or coordinates.' ;
/**
* Indicates if the tool is read-only (doesn't modify data).
*/
protected bool $readOnly = true ;
/**
* Define the tool's input schema.
*/
public function schema ( JsonSchema $schema ) : array
{
return [
'location' => $schema -> string ()
-> description ( 'City name, postal code, or coordinates (lat,lon)' )
-> required (),
'units' => $schema -> string ()
-> enum ([ 'celsius' , 'fahrenheit' ])
-> description ( 'Temperature units' )
-> default ( 'celsius' ),
];
}
/**
* Execute the tool.
*/
public function handle ( array $arguments ) : string
{
$location = $arguments [ 'location' ];
$units = $arguments [ 'units' ] ?? 'celsius' ;
// Extract API key from request header
$apiKey = request () -> header ( 'X-Weather-Api-Key' );
if ( ! $apiKey ) {
return "Error: Weather API key required. Include X-Weather-Api-Key header." ;
}
// Call external weather API
$response = Http :: get ( 'https://api.weatherapi.com/v1/current.json' , [
'key' => $apiKey ,
'q' => $location ,
]);
if ( ! $response -> successful ()) {
return "Error: Unable to fetch weather for { $location }" ;
}
$data = $response -> json ();
$temp = $units === 'fahrenheit'
? $data [ 'current' ][ 'temp_f' ] . '°F'
: $data [ 'current' ][ 'temp_c' ] . '°C' ;
return <<< MARKDOWN
## Weather in { $data ['location']['name']}, { $data ['location']['country']}
**Temperature:** { $temp }
**Condition:** { $data ['current']['condition']['text']}
**Humidity:** { $data ['current']['humidity']}%
**Wind:** { $data ['current']['wind_mph']} mph { $data ['current']['wind_dir']}
Last updated: { $data ['current']['last_updated']}
MARKDOWN ;
}
}
Update WeatherServer.php to include your tools:
protected array $tools = [
Tools\ GetWeather :: class ,
Tools\ GetForecast :: class ,
];
Step 4: Register the Server
Add your server to routes/ai.php:
use Laravel\Mcp\Facades\ Mcp ;
use App\Mcp\Servers\WeatherServer\ WeatherServer ;
Mcp :: web ( '/mcp/weather' , WeatherServer :: class );
Ensure routes/ai.php is loaded in bootstrap/app.php:
-> withRouting (
web : __DIR__ . '/../routes/web.php' ,
api : __DIR__ . '/../routes/api.php' ,
commands : __DIR__ . '/../routes/console.php' ,
health : '/up' ,
then : function () {
require __DIR__ . '/../routes/ai.php' ;
},
)
Schema Definition
Use JsonSchema to define tool parameters:
String Fields
'location' => $schema -> string ()
-> description ( 'City name or coordinates' )
-> required (),
'keyword' => $schema -> string ()
-> description ( 'Optional search keyword' )
-> default ( '' ),
Enum Fields
'units' => $schema -> string ()
-> enum ([ 'celsius' , 'fahrenheit' , 'kelvin' ])
-> description ( 'Temperature units' )
-> default ( 'celsius' ),
Integer Fields
'limit' => $schema -> integer ()
-> description ( 'Maximum number of results (1-100)' )
-> default ( 10 ),
Number Fields
'latitude' => $schema -> number ()
-> description ( 'Latitude coordinate (-90 to 90)' )
-> required (),
Boolean Fields
'include_forecast' => $schema -> boolean ()
-> description ( 'Include 5-day forecast' )
-> default ( false ),
Array Fields
'locations' => $schema -> array ()
-> description ( 'List of locations to check' )
-> required (),
Chainable Methods
Method Description ->description('...')Field description shown to AI ->required()Mark field as required ->default($value)Set default value ->enum(['a', 'b'])Restrict to specific string values
Not Available: Laravel’s JsonSchema does not support ->minimum(), ->maximum(), or enum classes. Document numeric limits in the description instead.
Authentication Patterns
Public (No Auth Required)
public function handle ( array $arguments ) : string
{
// No authentication needed
return "Result..." ;
}
public function handle ( array $arguments ) : string
{
$apiKey = request () -> header ( 'X-Api-Key' );
if ( ! $apiKey ) {
return "Error: API key required in X-Api-Key header" ;
}
// Use $apiKey...
}
Bearer Token
public function handle ( array $arguments ) : string
{
$token = request () -> bearerToken ();
if ( ! $token ) {
return "Error: Bearer token required" ;
}
// Validate $token...
}
Conversation-Scoped Auth
public function handle ( array $arguments ) : string
{
// Get conversation from bearer token
$token = request () -> bearerToken ();
$conversation = Conversation :: whereApiKey ( $token ) -> firstOrFail ();
// Access conversation-specific data
$reminders = $conversation -> reminders () -> get ();
return "Reminders..." ;
}
Best Practices
Do:
Keep tools focused on a single responsibility
Provide clear, detailed descriptions for AI context
Return structured output (markdown tables, lists)
Validate input parameters
Handle errors gracefully with clear messages
Don’t:
Create tools that do too many things
Return unstructured text blobs
Throw exceptions without handling them
Expose sensitive data without authorization
Return markdown-formatted strings for best LLM consumption:
return <<< MARKDOWN
## Weather Forecast
| Day | High | Low | Condition |
|-----|------|-----|-----------|
| Mon | 75°F | 55°F | Sunny |
| Tue | 72°F | 52°F | Cloudy |
| Wed | 68°F | 50°F | Rain |
**Note:** Temperatures in Fahrenheit. Bring an umbrella Wednesday!
MARKDOWN ;
Error Handling
try {
$response = Http :: get ( $url );
if ( ! $response -> successful ()) {
return "Error: API returned status { $response -> status ()}" ;
}
return "Success..." ;
} catch ( \ Exception $e ) {
return "Error: { $e -> getMessage ()}" ;
}
Service Classes
Extract complex logic into service classes:
// app/Mcp/Servers/WeatherServer/WeatherService.php
class WeatherService
{
public function getCurrentWeather ( string $location , string $apiKey ) : array
{
// API logic here
}
public function getForecast ( string $location , string $apiKey ) : array
{
// API logic here
}
}
// In your tool
public function handle ( array $arguments ) : string
{
$service = app ( WeatherService :: class );
$data = $service -> getCurrentWeather ( $arguments [ 'location' ], $apiKey );
return $this -> formatWeather ( $data );
}
Testing MCP Servers
Unit Tests
<? php
namespace Tests\Unit\Mcp\WeatherServer ;
use Tests\ TestCase ;
use App\Mcp\Servers\WeatherServer\Tools\ GetWeather ;
use Illuminate\Support\Facades\ Http ;
class GetWeatherTest extends TestCase
{
public function test_returns_weather_data () : void
{
Http :: fake ([
'api.weatherapi.com/*' => Http :: response ([
'location' => [ 'name' => 'Seattle' , 'country' => 'USA' ],
'current' => [
'temp_c' => 18 ,
'temp_f' => 64 ,
'condition' => [ 'text' => 'Partly cloudy' ],
'humidity' => 65 ,
'wind_mph' => 8 ,
'wind_dir' => 'NW' ,
'last_updated' => '2026-02-04 10:30' ,
],
]),
]);
request () -> headers -> set ( 'X-Weather-Api-Key' , 'test_key' );
$tool = new GetWeather ();
$result = $tool -> handle ([ 'location' => 'Seattle' , 'units' => 'celsius' ]);
$this -> assertStringContainsString ( 'Seattle' , $result );
$this -> assertStringContainsString ( '18°C' , $result );
}
}
Integration Testing with MCP Inspector
Use the MCP Inspector to test your server interactively:
npx @modelcontextprotocol/inspector
Connect to: http://localhost:8000/mcp/weather (Streamable HTTP transport)
Example: Complete CRUD Server
Here’s a simple CRUD server for managing notes:
// app/Mcp/Servers/NotesServer/NotesServer.php
class NotesServer extends Server
{
protected string $name = 'Notes' ;
protected string $version = '1.0.0' ;
protected string $instructions = 'CRUD operations for conversation notes' ;
protected array $tools = [
Tools\ CreateNote :: class ,
Tools\ ListNotes :: class ,
Tools\ UpdateNote :: class ,
Tools\ DeleteNote :: class ,
];
}
// app/Mcp/Servers/NotesServer/Tools/CreateNote.php
class CreateNote extends Tool
{
protected string $name = 'create-note' ;
protected string $description = 'Create a new note' ;
protected bool $readOnly = false ;
public function schema ( JsonSchema $schema ) : array
{
return [
'title' => $schema -> string () -> description ( 'Note title' ) -> required (),
'content' => $schema -> string () -> description ( 'Note content' ) -> required (),
];
}
public function handle ( array $arguments ) : string
{
$conversation = $this -> getConversation ();
$note = $conversation -> notes () -> create ([
'title' => $arguments [ 'title' ],
'content' => $arguments [ 'content' ],
]);
return "Note created successfully. ID: { $note -> id }" ;
}
private function getConversation ()
{
$token = request () -> bearerToken ();
return Conversation :: whereApiKey ( $token ) -> firstOrFail ();
}
}
Official Documentation
For complete Laravel MCP documentation:
Laravel MCP Documentation Official Laravel documentation for the MCP package
Current Platform Version: 1.79.0