Skip to main content

What is MCP?

Model Context Protocol (MCP) is a standardised protocol for communication between AI clients (Claude, Cursor, GitHub Copilot, etc.) and your application. By implementing an MCP server you let AI agents read your application’s data and execute actions on your behalf.
Laravel MCP is an official package added in Laravel 13, available as laravel/mcp. It provides everything you need to build MCP servers.
An MCP server can expose three types of capabilities:
CapabilityDescription
ToolsFunctions an AI client can call — search, update, or integrate with external APIs
ResourcesData and context an AI client can read
PromptsReusable, parameterised prompt templates

Installation

composer require laravel/mcp
After installation, publish the routes/ai.php file:
php artisan vendor:publish --tag=ai-routes
This creates routes/ai.php where you register your MCP servers.

Creating a server

Generate a server class with Artisan:
php artisan make:mcp-server WeatherServer
The class is placed in app/Mcp/Servers:
<?php

namespace App\Mcp\Servers;

use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
use Laravel\Mcp\Server;

#[Name('Weather Server')]
#[Version('1.0.0')]
#[Instructions('This server provides weather information and forecasts.')]
class WeatherServer extends Server
{
    protected array $tools = [
        // GetCurrentWeatherTool::class,
    ];

    protected array $resources = [
        // WeatherGuidelinesResource::class,
    ];

    protected array $prompts = [
        // DescribeWeatherPrompt::class,
    ];
}

Registering a server

Register servers in routes/ai.php. You can register a server as a web server or a local server.

Web server

A web server listens for HTTP POST requests. Use it for remote AI clients or web-based integrations.
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/weather', WeatherServer::class);
Apply middleware just like any other route:
Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware(['throttle:mcp']);

Local server

A local server runs as an Artisan command. Use it with local AI clients such as Claude Desktop.
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('weather', WeatherServer::class);
The MCP client normally starts local servers automatically — you do not need to run mcp:start manually.

Tools

Tools are functions an AI client can call. They can fetch data, update records, or integrate with external services.

Creating a tool

php artisan make:mcp-tool CurrentWeatherTool
Register the tool in the server’s $tools array:
use App\Mcp\Tools\CurrentWeatherTool;
use Laravel\Mcp\Server;

class WeatherServer extends Server
{
    protected array $tools = [
        CurrentWeatherTool::class,
    ];
}
A basic tool implementation:
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('Fetches the current weather forecast for a specified location.')]
class CurrentWeatherTool extends Tool
{
    public function handle(Request $request): Response
    {
        $location = $request->get('location');

        // Fetch weather data...

        return Response::text('The weather is sunny, 22°C.');
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'location' => $schema->string()
                ->description('The location to get the weather for.')
                ->required(),
        ];
    }
}

Tool name and description

The tool’s name and title are automatically derived from the class name. CurrentWeatherTool becomes name current-weather and title Current Weather Tool. Override with Name and Title attributes:
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;

#[Name('get-optimistic-weather')]
#[Title('Get Optimistic Weather Forecast')]
class CurrentWeatherTool extends Tool
{
    // ...
}
The Description attribute is not generated automatically. Always provide a meaningful description — it is how the AI model understands what the tool does and when to use it.

Input schema

Define input parameters in the schema method using Laravel’s JSON schema builder:
public function schema(JsonSchema $schema): array
{
    return [
        'location' => $schema->string()
            ->description('The location to get the weather for.')
            ->required(),

        'units' => $schema->string()
            ->enum(['celsius', 'fahrenheit'])
            ->description('The temperature units to use.')
            ->default('celsius'),
    ];
}

Output schema

Define the shape of the response in outputSchema so AI clients can parse it reliably:
public function outputSchema(JsonSchema $schema): array
{
    return [
        'temperature' => $schema->number()
            ->description('Temperature in Celsius')
            ->required(),

        'conditions' => $schema->string()
            ->description('Weather conditions')
            ->required(),

        'humidity' => $schema->integer()
            ->description('Humidity percentage')
            ->required(),
    ];
}

Validation

Use Laravel’s standard validation inside handle:
public function handle(Request $request): Response
{
    $validated = $request->validate([
        'location' => 'required|string|max:100',
        'units'    => 'in:celsius,fahrenheit',
    ], [
        'location.required' => 'You must specify a location, e.g. "New York City" or "Tokyo".',
        'units.in'          => 'Units must be "celsius" or "fahrenheit".',
    ]);

    // Use $validated...
}
When validation fails the AI client receives the error message and can retry. Write clear, actionable error messages to help the agent self-correct.

Dependency injection

Tools are resolved through the service container, so you can type-hint dependencies in the constructor or handle method:
<?php

namespace App\Mcp\Tools;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;

class CurrentWeatherTool extends Tool
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}

    public function handle(Request $request, WeatherRepository $weather): Response
    {
        $location = $request->get('location');
        $forecast = $weather->getForecastFor($location);

        return Response::text("Forecast: {$forecast}");
    }
}

Annotations

Add annotations to give AI clients additional hints about a tool’s behaviour:
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[IsIdempotent]
#[IsReadOnly]
class CurrentWeatherTool extends Tool
{
    // ...
}
AnnotationDescription
#[IsReadOnly]Tool does not modify any state
#[IsDestructive]Tool may perform destructive updates
#[IsIdempotent]Repeating the call with the same arguments has no additional side effects
#[IsOpenWorld]Tool may interact with external entities

Conditional registration

Implement shouldRegister to expose a tool only when certain conditions are met:
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->subscribed() ?? false;
}
When this returns false, the tool is invisible to the AI client.

Responses

Tools must return a Laravel\Mcp\Response instance.
return Response::text('Weather Summary: Sunny, 22°C');
return Response::error('Unable to fetch weather data. Please try again.');
return Response::image(file_get_contents(storage_path('weather/radar.png')), 'image/png');

return Response::audio(file_get_contents(storage_path('weather/alert.mp3')), 'audio/mp3');

// Load directly from storage (MIME type is auto-detected)
return Response::fromStorage('weather/radar.png');
public function handle(Request $request): array
{
    return [
        Response::text('Weather Summary: Sunny, 22°C'),
        Response::text("**Detailed Forecast**\n- Morning: 18°C\n- Afternoon: 25°C"),
    ];
}
return Response::structured([
    'temperature' => 22.5,
    'conditions'  => 'Partly cloudy',
    'humidity'    => 65,
]);
Stream progress updates for long-running operations:
public function handle(Request $request): Generator
{
    $locations = $request->array('locations');

    foreach ($locations as $index => $location) {
        yield Response::notification('processing/progress', [
            'current'  => $index + 1,
            'total'    => count($locations),
            'location' => $location,
        ]);

        yield Response::text($this->forecastFor($location));
    }
}

Prompts

Prompts are reusable, parameterised prompt templates that standardise common queries an AI client sends to a language model.

Creating a prompt

php artisan make:mcp-prompt DescribeWeatherPrompt
Register it in the server’s $prompts array:
use App\Mcp\Prompts\DescribeWeatherPrompt;

class WeatherServer extends Server
{
    protected array $prompts = [
        DescribeWeatherPrompt::class,
    ];
}

Prompt arguments

Define prompt parameters in the arguments method:
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;

class DescribeWeatherPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'tone',
                description: 'The tone for the weather description (e.g., formal, casual, humorous).',
                required: true,
            ),
        ];
    }
}

Prompt validation

Prompt arguments are automatically validated based on their definition, but you may enforce more complex validation rules by calling validate inside the handle method:
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'tone' => 'required|string|max:50',
        ]);

        $tone = $validated['tone'];

        // Generate the prompt response using the given tone...
    }
}
On validation failure, AI clients will act based on the error messages you provide. Provide clear and actionable messages:
$validated = $request->validate([
    'tone' => ['required', 'string', 'max:50'],
], [
    'tone.*' => 'You must specify a tone. Examples: "formal", "casual", "humorous".',
]);

Prompt dependency injection

The Laravel service container resolves all prompts, so you may type-hint dependencies in the constructor or handle method:
<?php

namespace App\Mcp\Prompts;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}
}
You may also type-hint dependencies in the handle method:
public function handle(Request $request, WeatherRepository $weather): Response
{
    $isAvailable = $weather->isServiceAvailable();

    // ...
}

Conditional prompt registration

Implement shouldRegister to conditionally register a prompt at runtime:
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Server\Prompt;

class CurrentWeatherPrompt extends Prompt
{
    public function shouldRegister(Request $request): bool
    {
        return $request?->user()?->subscribed() ?? false;
    }
}
When shouldRegister returns false, the prompt will not appear in the list of available prompts and cannot be invoked by AI clients.

Prompt responses

Return user and assistant messages from the handle method. Use asAssistant() to mark a message as coming from the assistant:
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function handle(Request $request): array
    {
        $tone = $request->string('tone');

        return [
            Response::text("You are a helpful weather assistant. Describe the weather in a {$tone} tone.")->asAssistant(),
            Response::text('What is the current weather like in Tokyo?'),
        ];
    }
}

Resources

Resources are data or information an AI client can load as context — documentation, configuration, or dynamic application data that improves the quality of AI responses.

Creating a resource

php artisan make:mcp-resource WeatherGuidelinesResource
Register it in the server’s $resources array:
use App\Mcp\Resources\WeatherGuidelinesResource;

class WeatherServer extends Server
{
    protected array $resources = [
        WeatherGuidelinesResource::class,
    ];
}
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Resource;

#[Description('Comprehensive guidelines for using the Weather API.')]
class WeatherGuidelinesResource extends Resource
{
    public function handle(Request $request): Response
    {
        $guidelines = "# Weather API Guidelines\n\n- Always specify a location...";

        return Response::text($guidelines);
    }
}

URI and MIME type

The URI is derived automatically from the class name (e.g. weather://resources/weather-guidelines). Customise with Uri and MimeType attributes:
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;

#[Uri('weather://resources/guidelines')]
#[MimeType('application/pdf')]
class WeatherGuidelinesResource extends Resource
{
    // ...
}

Resource templates

Implement HasUriTemplate to define dynamic resources with URI variables:
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Support\UriTemplate;

#[Description('Access user files by ID')]
#[MimeType('text/plain')]
class UserFileResource extends Resource implements HasUriTemplate
{
    public function uriTemplate(): UriTemplate
    {
        return new UriTemplate('file://users/{userId}/files/{fileId}');
    }

    public function handle(Request $request): Response
    {
        $userId = $request->get('userId');
        $fileId = $request->get('fileId');

        return Response::text("File {$fileId} for user {$userId}");
    }
}
URI variables are automatically injected into the request and accessible via get().

Resource request

Unlike tools and prompts, resources cannot define input schemas or arguments. However, you can still interact with the request object within your resource’s handle method:
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function handle(Request $request): Response
    {
        // Access request information...
    }
}

Resource dependency injection

The Laravel service container resolves all resources, so you may type-hint dependencies in the constructor or handle method:
<?php

namespace App\Mcp\Resources;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}
}
You may also type-hint dependencies in the handle method:
public function handle(WeatherRepository $weather): Response
{
    return Response::text($weather->guidelines());
}

Resource annotations

Annotate resources with audience, priority, and last-modified date:
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\LastModified;
use Laravel\Mcp\Server\Annotations\Priority;
use Laravel\Mcp\Server\Resource;

#[Audience(Role::User)]
#[LastModified('2025-01-12T15:00:58Z')]
#[Priority(0.9)]
class UserDashboardResource extends Resource
{
    // ...
}
AnnotationTypeDescription
#[Audience]Role or arrayIntended audience (Role::User, Role::Assistant, or both)
#[Priority]floatImportance score from 0.0 to 1.0
#[LastModified]stringISO 8601 timestamp of the last modification

Conditional resource registration

Implement shouldRegister to conditionally register a resource at runtime:
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function shouldRegister(Request $request): bool
    {
        return $request?->user()?->subscribed() ?? false;
    }
}
When shouldRegister returns false, the resource will not appear in the available list and cannot be accessed by AI clients.

Resource responses

Resources must return an instance of Laravel\Mcp\Response. For text content, use the text method:
return Response::text($weatherData);
Use resourceLink to return a URI pointer that the AI client fetches independently, rather than embedded content:
return Response::resourceLink(
    uri: 'file:///data/report.json',
    name: 'monthly-report',
    mimeType: 'application/json',
);
You may also pass a registered resource class or instance to automatically inherit its URI, name, title, description, and MIME type:
return Response::resourceLink(new WeatherForecastResource);

Blob responses

Return binary content using the blob method. The MIME type is determined by the resource’s #[MimeType] attribute:
return Response::blob(file_get_contents(storage_path('weather/radar.png')));
#[MimeType('image/png')]
class WeatherGuidelinesResource extends Resource
{
    // ...
}

Error responses

Indicate an error using the error method:
return Response::error('Unable to fetch weather data for the specified location.');

Apps

Laravel MCP supports MCP Apps, an extension of the Model Context Protocol that allows tools to render interactive HTML applications within sandboxed iframes in supported hosts. This allows you to build dashboards, forms, visualizations, and other rich experiences that go beyond plain text responses. An MCP app consists of two parts working together:
  • An app resource that returns the self-contained HTML for your application.
  • A tool that is linked to the app resource using the #[RendersApp] attribute. When the tool is called, the host fetches and renders the linked resource.

Creating app resources

Create an app resource using the make:mcp-app-resource Artisan command:
php artisan make:mcp-app-resource WeatherDashboardApp
This command creates two files: a PHP class in app/Mcp/Resources and a Blade view in resources/views/mcp. The view name is automatically inferred from the class name. For example, WeatherDashboardApp maps to mcp.weather-dashboard-app:
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\AppMeta;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\AppResource;

#[Description('An interactive weather dashboard.')]
#[AppMeta]
class WeatherDashboardApp extends AppResource
{
    /**
     * Handle the app resource request.
     */
    public function handle(Request $request): Response
    {
        return Response::view('mcp.weather-dashboard-app', [
            'title' => $this->title(),
        ]);
    }
}
AppResource extends the base Resource class and automatically configures the ui:// URI scheme and the text/html;profile=mcp-app MIME type required by the MCP Apps specification. Like any other resource, you must register it in your server’s $resources array. The generated Blade view uses the <x-mcp::app> component, which renders a complete HTML document with the client-side MCP SDK bundled and ready to use:
<x-mcp::app :title="$title">
    <x-slot:head>
        <script type="module">
        createMcpApp(async (app) => {
            document.getElementById('run-btn').addEventListener('click', async () => {
                const result = await app.callServerTool('get-weather-data', {});
                document.getElementById('output').textContent = result.content[0]?.text ?? '';
            });
        });
        </script>
    </x-slot:head>

    <div id="app">
        <button id="run-btn">Refresh</button>
        <p id="output"></p>
    </div>
</x-mcp::app>
The createMcpApp global is provided by the bundled SDK and handles connecting the iframe to the server, applying host theming, and exposing helpers such as callServerTool, sendMessage, openLink, and event callbacks. For the full client-side API, refer to the MCP Apps specification.

Rendering apps from tools

To display an app resource, link a tool to it using the #[RendersApp] attribute. When the tool is called, Laravel MCP includes the resource’s URI in the tool metadata so the host can render the app in a sandboxed iframe:
<?php

namespace App\Mcp\Tools;

use App\Mcp\Resources\WeatherDashboardApp;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\RendersApp;
use Laravel\Mcp\Server\Tool;

#[RendersApp(resource: WeatherDashboardApp::class)]
class ShowWeatherDashboard extends Tool
{
    /**
     * Handle the tool request.
     */
    public function handle(Request $request): Response
    {
        return Response::text('Weather dashboard loaded.');
    }
}
Laravel MCP automatically advertises the io.modelcontextprotocol/ui capability whenever any AppResource is registered, so no additional server configuration is required.

App tool visibility

Each #[RendersApp] tool can limit who may invoke it via the visibility argument. This is useful for exposing private, app-only tools that the UI calls to load or refresh data without making those tools visible to the model:
use Laravel\Mcp\Server\Attributes\RendersApp;
use Laravel\Mcp\Server\Ui\Enums\Visibility;

#[RendersApp(resource: WeatherDashboardApp::class, visibility: [Visibility::App])]
class GetWeatherData extends Tool
{
    // ...
}
The Visibility enum has two cases, Model and App, and defaults to both. Use [Visibility::App] for backend actions the UI calls directly, or [Visibility::Model] to make a tool unavailable to the UI.

App configuration

The #[AppMeta] attribute on your app resource configures the iframe’s Content Security Policy, browser permissions, and any library scripts that should be included in the view’s <head>:
use Laravel\Mcp\Server\Attributes\AppMeta;
use Laravel\Mcp\Server\Ui\Enums\Library;
use Laravel\Mcp\Server\Ui\Enums\Permission;

#[AppMeta(
    connectDomains: ['https://api.weather.com'],
    permissions: [Permission::Geolocation],
    libraries: [Library::Tailwind, Library::Alpine],
)]
class WeatherDashboardApp extends AppResource
{
    // ...
}
The Library enum includes pre-configured CDN scripts for common front-end libraries such as Library::Tailwind and Library::Alpine, and their CDN origins are automatically merged into the CSP. The Permission enum covers browser permissions such as Camera, Microphone, Geolocation, and ClipboardWrite.
For computed or dynamic configuration, override the appMeta method on your resource using the fluent AppMeta, Csp, and Permissions builders from the Laravel\Mcp\Server\Ui namespace.

Building apps with Boost

Laravel MCP includes a dedicated Boost skill reference for building MCP Apps. If you have Laravel Boost installed, your AI coding agent can invoke the mcp-development skill and ask it to scaffold an app resource, Blade view, and linked tool for you. For the complete protocol reference, including the full client-side API and schema details, see the official MCP Apps documentation.

Metadata

Attach MCP spec _meta fields to tool, resource, and prompt responses:
return Response::text('The weather is sunny.')
    ->withMeta(['source' => 'weather-api', 'cached' => true]);
To attach metadata to the response envelope, use Response::make:
return Response::make(
    Response::text('The weather is sunny.')
)->withMeta(['request_id' => '12345']);
To attach metadata to the tool, resource, or prompt class itself, define a $meta property:
class CurrentWeatherTool extends Tool
{
    protected ?array $meta = [
        'version' => '2.0',
        'author'  => 'Weather Team',
    ];
}

Icons

MCP clients can display icons for your server and its primitives. Declare icons on a server, tool, resource, or prompt using the Icon attribute:
use Laravel\Mcp\Enums\IconTheme;
use Laravel\Mcp\Server\Attributes\Icon;

#[Icon('mcp/server.png', mimeType: 'image/png', sizes: ['48x48'])]
#[Icon('mcp/server-dark.svg', theme: IconTheme::Dark)]
class WeatherServer extends Server
{
    // ...
}
The Icon attribute is repeatable, so you may declare multiple icons to provide different sizes or light and dark theme variants. Alternatively, define icons programmatically by overriding the icons method. This is useful when an icon depends on runtime conditions:
use Laravel\Mcp\Schema\Icon;

class CurrentWeatherTool extends Tool
{
    /**
     * Get the tool's icons.
     *
     * @return array<int, Icon>
     */
    public function icons(): array
    {
        return [
            Icon::from('mcp/tool.png', mimeType: 'image/png'),
        ];
    }
}
Icons defined via the attribute and the icons method are combined automatically. Icon paths are resolved as follows:
  • Paths with a URI scheme such as https: or data: are used as-is.
  • Relative paths are resolved to a URL using Laravel’s asset helper.

Authentication

Web servers support Laravel’s standard middleware for authentication.

Sanctum

Use token-based authentication with Laravel Sanctum. The MCP client sends an Authorization: Bearer <token> header.
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware('auth:sanctum');

OAuth 2.1

Use Laravel Passport for robust OAuth-based authentication:
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware('auth:api');
When using OAuth, publish the MCP authorization views and register them with Passport:
php artisan vendor:publish --tag=mcp-views
// AppServiceProvider::boot()
use Laravel\Passport\Passport;

Passport::authorizationView(function ($parameters) {
    return view('mcp.authorize', $parameters);
});

Authorization

Access the authenticated user inside a tool or resource via $request->user():
public function handle(Request $request): Response
{
    if (! $request->user()->can('read-weather')) {
        return Response::error('Permission denied.');
    }

    // Continue...
}

MCP Client

In addition to building servers, Laravel MCP includes a client for connecting to other MCP servers, whether first-party or third-party. The client lets your application discover and call the tools exposed by an MCP server, which is especially useful for giving your AI agents access to capabilities provided by external MCP servers.

Connecting to Servers

Connect to an HTTP-accessible MCP server using the Client::web method, passing the server’s URL:
use Laravel\Mcp\Client;

$client = Client::web('https://mcp.example.com');
To connect to a local MCP server that runs as a command, use the Client::local method, providing the command and any arguments needed to start the server:
use Laravel\Mcp\Client;

$client = Client::local('php', ['artisan', 'mcp:start']);
The client connects lazily, automatically establishing the connection the first time you list or call tools. If you need to manage the connection manually, you may use the connect, connected, ping, and disconnect methods:
$client->connect();

$client->ping();

if ($client->connected()) {
    // ...
}

$client->disconnect();
Customize the request timeout using the withTimeout method:
$client = Client::web('https://mcp.example.com')->withTimeout(30);

Named Clients

Instead of constructing a client each time, register reusable named clients in the boot method of a service provider using the Mcp facade:
use Laravel\Mcp\Client;
use Laravel\Mcp\Facades\Mcp;

Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com'));
Resolve the client anywhere in your application by name:
use Laravel\Mcp\Facades\Mcp;

$client = Mcp::client('github');
Named clients are resolved once per request and automatically disconnected at the end of the request lifecycle.

Client Authentication

To connect to a web MCP server protected by a bearer token, use the withToken method. You may pass a token string or a closure that lazily resolves the token:
use Illuminate\Support\Facades\Auth;
use Laravel\Mcp\Client;

$client = Client::web('https://mcp.example.com')->withToken($token);

$client = Client::web('https://mcp.example.com')->withToken(
    fn () => Auth::user()->mcpToken(),
);
For servers protected by OAuth 2.1, configure the client using the withOAuth method:
use Laravel\Mcp\Client;
use Laravel\Mcp\Facades\Mcp;

Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com')->withOAuth(
    clientId: config('services.github_mcp.client_id'),
    clientSecret: config('services.github_mcp.client_secret'),
));
The clientId and clientSecret arguments may be omitted when the MCP server supports dynamic client registration, in which case the client registers itself automatically.
Register the OAuth routes for the named client in routes/ai.php using the oAuthRoutesFor method:
use Illuminate\Support\Facades\Auth;
use Laravel\Mcp\Client\OAuth\TokenSet;
use Laravel\Mcp\Facades\Mcp;

Mcp::oAuthRoutesFor('github', function (string $client, TokenSet $token) {
    Auth::user()->update([
        'github_mcp_token' => $token->accessToken,
    ]);

    return redirect('/dashboard');
});
This registers two named routes: a connect route (mcp.oauth.{client}.connect) that redirects the user to the authorization server, and a callback route (mcp.oauth.{client}.callback) that exchanges the authorization code and invokes your handler. To begin the authorization flow, redirect the user to the connect route:
return redirect()->route('mcp.oauth.github.connect');

Tools

Retrieve the tools exposed by an MCP server using the tools method, which returns a collection keyed by tool name:
use Laravel\Mcp\Facades\Mcp;

$tools = Mcp::client('github')->tools();

foreach ($tools as $tool) {
    $tool->name;
    $tool->title;
    $tool->description;
    $tool->inputSchema;
}
The client automatically paginates through all available tools. Limit the number returned using the limit argument:
$tools = Mcp::client('github')->tools(limit: 10);
Invoke a tool with the callTool method, passing the tool name and an array of arguments:
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->callTool('current-weather', [
    'location' => 'New York',
]);

$result->text();             // The text content of the response
(string) $result;            // Equivalent to calling text()
$result->isError;            // Whether the tool reported an error
$result->structuredContent;  // Structured content, if any
You may also call a tool directly from a listed tool instance:
$tools = Mcp::client('github')->tools();

$result = $tools['current-weather']->call([
    'location' => 'New York',
]);
If you are building agents with the Laravel AI SDK, you may provide tools from an MCP client directly to an agent. See the MCP Tools section of the AI SDK documentation for more information.

Prompts

Retrieve the prompts exposed by an MCP server using the prompts method, which returns a collection keyed by prompt name:
use Laravel\Mcp\Facades\Mcp;

$prompts = Mcp::client('github')->prompts();

foreach ($prompts as $prompt) {
    $prompt->name;
    $prompt->title;
    $prompt->description;
    $prompt->arguments;
}
The client automatically paginates through all available prompts. Limit the number returned using the limit argument:
$prompts = Mcp::client('github')->prompts(limit: 10);
To retrieve a prompt, use the getPrompt method, passing the prompt name and an array of arguments. The returned PromptResult instance exposes the generated messages:
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->getPrompt('describe-weather', [
    'location' => 'New York',
]);

$result->text();        // The text content of the messages
(string) $result;       // Equivalent to calling text()
$result->messages;      // The raw messages returned by the prompt
$result->description;   // The prompt description, if any

Resources

Retrieve the resources exposed by an MCP server using the resources method, which returns a collection keyed by URI:
use Laravel\Mcp\Facades\Mcp;

$resources = Mcp::client('github')->resources();

foreach ($resources as $resource) {
    $resource->uri;
    $resource->name;
    $resource->title;
    $resource->description;
    $resource->mimeType;
    $resource->size;
}
The client automatically paginates through all available resources. Limit the number returned using the limit argument:
$resources = Mcp::client('github')->resources(limit: 10);
To read a resource, use the readResource method, passing the resource URI. The returned ResourceReadResult instance exposes the resource content:
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->readResource('weather://guidelines');

$result->content();   // The content of the resource, decoding base64 blobs as needed
(string) $result;     // Equivalent to calling content()
$result->mimeType();  // The MIME type of the resource, if any
$result->contents;    // The raw contents returned by the resource

Testing

MCP Inspector

Use the interactive mcp:inspector command to debug your server:
# Web server
php artisan mcp:inspector mcp/weather

# Local server (named 'weather')
php artisan mcp:inspector weather
The command launches the MCP Inspector and displays a client configuration you can copy. If your server uses authentication middleware, include the Authorization header when connecting.

Unit tests

Write unit tests directly against your tools, resources, and prompts:
test('tool', function () {
    $response = WeatherServer::tool(CurrentWeatherTool::class, [
        'location' => 'Tokyo',
        'units'    => 'celsius',
    ]);

    $response
        ->assertOk()
        ->assertSee('The current weather in Tokyo is 22°C and sunny.');
});
Prompts and resources follow the same pattern:
$response = WeatherServer::prompt(DescribeWeatherPrompt::class, ['tone' => 'casual']);
$response = WeatherServer::resource(WeatherGuidelinesResource::class);
Authenticate as a specific user with actingAs:
$response = WeatherServer::actingAs($user)->tool(CurrentWeatherTool::class, [...]);
Key assertion methods:
$response->assertOk();        // No error in the response
$response->assertSee('...');  // Response contains the given text
Assert that an error is present:
$response->assertHasErrors();

$response->assertHasErrors([
    'Something went wrong.',
]);
Assert that no errors are present:
$response->assertHasNoErrors();
Assert the name, title, or description of the tool, resource, or prompt:
$response->assertName('current-weather');
$response->assertTitle('Current Weather Tool');
$response->assertDescription('Fetches the current weather forecast for a specified location.');
Assert streaming notifications using assertSentNotification and assertNotificationCount:
$response->assertSentNotification('processing/progress', [
    'step' => 1,
    'total' => 5,
]);

$response->assertSentNotification('processing/progress', [
    'step' => 2,
    'total' => 5,
]);

$response->assertNotificationCount(5);
Inspect the raw response for debugging:
$response->dd();
$response->dump();
Last modified on June 22, 2026