> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chatbotplatform.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Building Custom MCP Servers

> Create your own Model Context Protocol servers using Laravel's MCP package to extend AI capabilities with custom tools.

## 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:

```bash theme={null}
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

```bash theme={null}
php artisan make:mcp-server WeatherServer
```

This creates `app/Mcp/Servers/WeatherServer/WeatherServer.php`:

```php theme={null}
<?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:

```bash theme={null}
php artisan make:mcp-tool GetWeather --server=WeatherServer
```

This creates `app/Mcp/Servers/WeatherServer/Tools/GetWeather.php`:

```php theme={null}
<?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:

```php theme={null}
protected array $tools = [
    Tools\GetWeather::class,
    Tools\GetForecast::class,
];
```

### Step 4: Register the Server

Add your server to `routes/ai.php`:

```php theme={null}
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`:

```php theme={null}
->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

```php theme={null}
'location' => $schema->string()
    ->description('City name or coordinates')
    ->required(),

'keyword' => $schema->string()
    ->description('Optional search keyword')
    ->default(''),
```

### Enum Fields

```php theme={null}
'units' => $schema->string()
    ->enum(['celsius', 'fahrenheit', 'kelvin'])
    ->description('Temperature units')
    ->default('celsius'),
```

### Integer Fields

```php theme={null}
'limit' => $schema->integer()
    ->description('Maximum number of results (1-100)')
    ->default(10),
```

### Number Fields

```php theme={null}
'latitude' => $schema->number()
    ->description('Latitude coordinate (-90 to 90)')
    ->required(),
```

### Boolean Fields

```php theme={null}
'include_forecast' => $schema->boolean()
    ->description('Include 5-day forecast')
    ->default(false),
```

### Array Fields

```php theme={null}
'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 |

<Callout type="warning">
  **Not Available:** Laravel's JsonSchema does not support `->minimum()`, `->maximum()`, or enum classes. Document numeric limits in the description instead.
</Callout>

## Authentication Patterns

### Public (No Auth Required)

```php theme={null}
public function handle(array $arguments): string
{
    // No authentication needed
    return "Result...";
}
```

### Custom Header

```php theme={null}
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

```php theme={null}
public function handle(array $arguments): string
{
    $token = request()->bearerToken();

    if (!$token) {
        return "Error: Bearer token required";
    }

    // Validate $token...
}
```

### Conversation-Scoped Auth

```php theme={null}
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:

```php theme={null}
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

```php theme={null}
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:

```php theme={null}
// 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 theme={null}
<?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:

```bash theme={null}
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:

```php theme={null}
// 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:

<Card title="Laravel MCP Documentation" icon="laravel" href="https://laravel.com/docs/12.x/mcp">
  Official Laravel documentation for the MCP package
</Card>

***

**Current Platform Version:** 1.79.0
