Skip to main content

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 = [];
}

Step 2: Create Tools

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;
    }
}

Step 3: Register Tools in Server

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

MethodDescription
->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...";
}

Custom Header

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

Tool Design

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 Format

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