An assistant can explain an application beautifully and still be unable to do anything useful with it. It can describe the workflow, suggest the next step, and produce a convincing answer. Then someone has to copy that answer into the actual system.

In Conducting the orchestra, the application supplied the score: routing work between models, carrying context forward, and deciding what to do with their output. There is another part of that relationship. An assistant also needs a clear way to ask the application for information or request an operation.

That is where Laravel MCP becomes interesting. The framework can keep its existing business rules while offering a deliberate interface to AI clients. The challenge is choosing that interface carefully enough that the useful abstraction survives contact with an uncertain caller.

A capability deserves a contract

MCP gives a client a way to discover a server’s tools and call them with structured arguments. The client can make those capabilities available to an assistant, rather than leaving the assistant to invent how an application works. A tool’s name, description, and input schema become part of the interaction. MCP tool specification.

I would start with a capability a human could describe in one sentence. A small operation is easier to name, easier to validate, and easier to understand when it fails. A large operation with an arbitrary query string moves too much of the application’s meaning into the caller’s interpretation.

This also changes how a tool description should be written. “Process data” tells the client almost nothing. “Convert a Celsius temperature to Fahrenheit without reading or changing application data” tells it what the operation accepts and what sort of result it can expect. The description is part of an interface, so it deserves the same care as an endpoint’s contract.

Laravel MCP follows familiar conventions: server classes register capabilities, tool classes handle requests, and HTTP registration can use Laravel middleware. The examples here target the released laravel/mcp 1.0.1 API, checked against the current Laravel 13.x documentation. Laravel MCP documentation, release 1.0.1.

Start with something deliberately ordinary

Consider a temperature converter. It is a made-up teaching example, with no application database, external service, or product workflow behind it. That keeps attention on the boundary: one number in, a predictable result out, and a clear response when the input makes no sense.

Install the package and generate the server and tool inside an existing Laravel application:

composer require laravel/mcp:^1.0
php artisan vendor:publish --tag=ai-routes
php artisan make:mcp-server ConversionServer
php artisan make:mcp-tool ConvertTemperature

The tool goes in app/Mcp/Tools/ConvertTemperature.php:

<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;

#[Name('convert-temperature')]
#[Description('Convert a Celsius temperature to Fahrenheit without reading or changing application data.')]
#[IsReadOnly]
class ConvertTemperature extends Tool
{
    public function handle(Request $request): ResponseFactory
    {
        $validated = $request->validate([
            'celsius' => ['required', 'numeric:strict', 'between:-273.15,1000000'],
        ]);

        $celsius = (float) $validated['celsius'];

        return Response::structured([
            'celsius' => $celsius,
            'fahrenheit' => round(($celsius * 9 / 5) + 32, 2),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'celsius' => $schema->number()
                ->description('Temperature in degrees Celsius, from -273.15 to 1000000.')
                ->min(-273.15)
                ->max(1000000)
                ->required(),
        ];
    }

    public function outputSchema(JsonSchema $schema): array
    {
        return [
            'celsius' => $schema->number()->required(),
            'fahrenheit' => $schema->number()->required(),
        ];
    }
}

The schema explains the accepted shape to a client. Validation checks the incoming value in the handler. Keeping both explicit makes the intended contract visible while ensuring the application checks what actually arrives. Strict numeric validation also rejects a string masquerading as a JSON number. The upper bound is an explicit limit for this example, keeping arithmetic and serialization within a small supported range. A real capability should define its own range. The conversion returns structured data so the caller can use the result without extracting a number from a sentence. In 1.0.1, Response::structured() returns a ResponseFactory, so the handler uses that return type. Released response API.

The registered server goes in app/Mcp/Servers/ConversionServer.php:

<?php

namespace App\Mcp\Servers;

use App\Mcp\Tools\ConvertTemperature;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;

#[Name('Conversion Server')]
#[Version('1.0.0')]
#[Instructions('Convert supplied temperatures using the registered tool. No application records are exposed.')]
class ConversionServer extends Server
{
    protected array $tools = [ConvertTemperature::class];
}

For a local development connection, register it in routes/ai.php:

<?php

use App\Mcp\Servers\ConversionServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('conversions', ConversionServer::class);

An MCP client can launch that local server. The Inspector is another way to exercise it during development:

php artisan mcp:inspector conversions

The point of this example is how little judgment the operation needs from the caller. The input has a unit. The output has named fields. The handler owns the calculation. A caller that supplies an invalid value gets an error instead of a plausible-looking conversion.

Decide what belongs in the interface

Tools are only one of MCP’s primitives. A resource provides addressable context, such as a document at a URI, which the client decides how to use. A prompt offers a named message template with arguments that can guide an interaction. Each has a different job, and each deserves a deliberate decision about what the application should expose. MCP resources, MCP prompts.

I would avoid exposing every available primitive just to demonstrate the protocol. A client that needs one calculation gets a clearer interface from one tool than from a collection of loosely related helpers. The number of capabilities is a maintenance decision: every additional contract creates another place where naming, permissions, and behavior can drift apart.

The same reasoning applies to the information a capability returns. A client may need a few fields rather than a complete application record. Returning only what the operation needs makes the response easier to interpret and gives the application a smaller disclosure boundary to maintain.

Permissions stay with the application

The converter’s IsReadOnly annotation describes its intended behavior. It does not enforce that behavior. The protocol treats tool annotations as hints, and warns clients about trusting metadata from untrusted servers. The application still controls the handler’s effects. MCP tool annotations.

A tool that reads account data needs the same ownership and authorization checks as a normal application route. A tool that changes a record needs a policy for that operation. Instructions telling an assistant to be careful cannot replace either check.

For an HTTP server in an application that already uses Sanctum, Laravel MCP documents registration behind auth:sanctum. That authenticates the caller; the handler still needs to decide whether that caller may perform the specific operation. OAuth is another documented option where the client integration calls for it. Laravel MCP authentication and authorization.

There is a second boundary when the information returned to an assistant contains arbitrary text. A document can include instructions written by someone else. I would treat that text as evidence and keep it separate from the authority to invoke tools. The specification calls for clients to validate tool results before passing them to a model. A structured protocol does not make the content inside it trustworthy. MCP tool security considerations.

For operations with side effects, I would define retry behavior before giving the tool to a client. A connection can fail after an operation succeeds. An assistant can also decide to call a tool again. An operation that creates a second record on retry needs an explicit way to recognize the same request and return the existing result. That is ordinary application engineering with a new kind of caller.

Test the contract before the conversation

A useful first test does not need a model. In a Laravel test case, the package’s testing API can call a registered tool directly:

use App\Mcp\Servers\ConversionServer;
use App\Mcp\Tools\ConvertTemperature;

public function test_zero_celsius_converts_to_thirty_two_fahrenheit(): void
{
    $response = ConversionServer::tool(ConvertTemperature::class, [
        'celsius' => 0,
    ]);

    $response->assertOk()->assertSee('"fahrenheit":32');
}

I would also check the value where both scales meet, a missing argument, a value that is not a number, a numeric string, and temperatures outside the declared range. These cases establish behavior at the boundary before asking an assistant to choose the tool. Laravel MCP testing API.

That still leaves the conversation to test. Can the client discover the tool? Does the description distinguish it from nearby capabilities? Does the assistant choose it when appropriate and understand the validation error when it supplies bad input? A deterministic test can establish what the application does. It cannot establish every decision a model will make around it.


The score still belongs to the application. Laravel MCP gives it an interface that an AI client can understand, and the useful engineering is in making that interface explicit: small capabilities, readable contracts, enforced permissions, and behavior that survives an unsuccessful request. The protocol supplies a shared language. The application supplies the meaning.

Andy Fraussen

Back to thoughts