opgginc/laravel-mcp-server

This is my package laravel-mcp-server

Maintainers

👁 kargnas

Package info

github.com/opgginc/laravel-mcp-server

pkg:composer/opgginc/laravel-mcp-server

Statistics

Installs: 64 645

Dependents: 2

Suggesters: 0

Stars: 332

Open Issues: 4

v2.0.6 2026-04-05 11:33 UTC

Requires

Suggests

None

Provides

None

Conflicts

None

Replaces

None

MIT f7a7e351ed6450c251779259ab6b389921c30ec9

laravelopgginclaravel-mcp-server


README

Build a route-first MCP server in Laravel and Lumen

👁 Build Status
👁 Total Downloads
👁 Latest Stable Version
👁 License

Official Website

English | Português do Brasil | 한국어 | Русский | 简体中文 | 繁體中文 | Polski | Español

👁 Laravel MCP Server Demo

Breaking Changes 2.0.0

  • Endpoint setup moved from config-driven registration to route-driven registration.
  • Streamable HTTP is the only supported transport.
  • Server metadata mutators are consolidated into setServerInfo(...).
  • Legacy tool transport methods were removed from runtime (messageType(), ProcessMessageType::SSE).

Full migration guide: docs/migrations/v2.0.0-migration.md

Overview

Laravel MCP Server provides route-based MCP endpoint registration for Laravel and Lumen.

Key points:

  • Streamable HTTP transport
  • Route-first configuration (Route::mcp(...) / McpRoute::register(...))
  • Tool, resource, resource template, and prompt registration per endpoint
  • Route cache compatible endpoint metadata

Requirements

  • PHP >= 8.2
  • Laravel (Illuminate) >= 9.x
  • Lumen >= 9.x (optional)

Quick Start

1) Install

composer require opgginc/laravel-mcp-server

2) Register an endpoint (Laravel)

use Illuminate\Support\Facades\Route;
use OPGG\LaravelMcpServer\Enums\ProtocolVersion;
use OPGG\LaravelMcpServer\Services\ToolService\Examples\HelloWorldTool;
use OPGG\LaravelMcpServer\Services\ToolService\Examples\VersionCheckTool;

Route::mcp('/mcp')
 ->setServerInfo(
 name: 'OP.GG MCP Server',
 version: '2.0.0',
 )
 ->setConfig(
 compactEnumExampleCount: 3,
 )
 ->setProtocolVersion(ProtocolVersion::V2025_11_25)
 ->enabledApi()
 ->tools([
 HelloWorldTool::class,
 VersionCheckTool::class,
 ]);

If you need compatibility with clients that do not support 2025-11-25, set:

->setProtocolVersion(ProtocolVersion::V2025_06_18)

3) Verify

php artisan route:list | grep mcp
php artisan mcp:test-tool --list --endpoint=/mcp

Quick JSON-RPC check:

curl -X POST http://localhost:8000/mcp \
 -H "Content-Type: application/json" \
 -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Dynamic Tool Filtering by Query String

If one endpoint needs to expose different tool sets based on the incoming URL, attach a dynamic tools resolver to the route. The resolver owns both the declared tool catalog for the endpoint and the per-request visible subset.

use OPGG\LaravelMcpServer\Data\ToolResolutionContext;
use OPGG\LaravelMcpServer\Routing\McpEndpointDefinition;
use OPGG\LaravelMcpServer\Services\ToolService\DynamicToolResolverInterface;

final class LolPhaseToolResolver implements DynamicToolResolverInterface
{
 public function declaredTools(McpEndpointDefinition $endpoint): array
 {
 return [
 \App\MCP\Tools\LolSearchChampionMetaTool::class,
 \App\MCP\Tools\LolGetChampionAnalysisTool::class,
 \App\MCP\Tools\LolGetLiveItemRecommendationsTool::class,
 ];
 }

 public function resolve(
 McpEndpointDefinition $endpoint,
 ToolResolutionContext $context,
 ): array {
 return match ($context->queryParameters['phase'] ?? null) {
 'lobby' => [
 \App\MCP\Tools\LolSearchChampionMetaTool::class,
 \App\MCP\Tools\LolGetChampionAnalysisTool::class,
 ],
 'inprogress' => [
 \App\MCP\Tools\LolGetLiveItemRecommendationsTool::class,
 ],
 default => $this->declaredTools($endpoint),
 };
 }

 public function consumedQueryParameters(): array
 {
 return ['phase'];
 }
}
Route::mcp('/mcp/voice/lol/live')
 ->setServerInfo(
 name: 'OP.GG MCP Server - Voice lol Live',
 version: '1.0.0',
 )
 ->dynamicTools(LolPhaseToolResolver::class);

Example requests:

/mcp/voice/lol/live?phase=lobby
/mcp/voice/lol/live?phase=inprogress

The same filtered tool set is applied consistently to:

  • tools/list
  • tools/call
  • tools/execute
  • POST /tools/{tool_name} when ->enabledApi() is enabled

If the same endpoint also uses POST /tools/{tool_name}, you can optionally expose a public consumedQueryParameters(): array hook on the resolver for query keys that should be used only for filtering and not forwarded as tool arguments. This hook is a documented convention and is not part of DynamicToolResolverInterface; resolvers that omit it will forward those query keys as tool arguments.

Lumen Setup

// bootstrap/app.php
$app->withFacades();
$app->withEloquent();
$app->register(OPGG\LaravelMcpServer\LaravelMcpServerServiceProvider::class);
use OPGG\LaravelMcpServer\Routing\McpRoute;
use OPGG\LaravelMcpServer\Services\ToolService\Examples\HelloWorldTool;

McpRoute::register('/mcp')
 ->setServerInfo(
 name: 'OP.GG MCP Server',
 version: '2.0.0',
 )
 ->tools([
 HelloWorldTool::class,
 ]);

Minimal Security (Production)

Use Laravel middleware on your MCP route group.

use Illuminate\Support\Facades\Route;

Route::middleware([
 'auth:sanctum',
 'throttle:100,1',
])->group(function (): void {
 Route::mcp('/mcp')
 ->setServerInfo(
 name: 'Secure MCP',
 version: '2.0.0',
 )
 ->tools([
 \App\MCP\Tools\MyCustomTool::class,
 ]);
});

v2.0.0 Migration Notes (from v1.0.0)

  • MCP endpoint setup moved from config to route registration.
  • Streamable HTTP is the only transport.
  • Server metadata mutators are consolidated into setServerInfo(...).
  • Tool migration command is available for legacy signatures:
php artisan mcp:migrate-tools

Full guide: docs/migrations/v2.0.0-migration.md

Advanced Features (Quick Links)

  • Create tools: php artisan make:mcp-tool ToolName
  • Create resources: php artisan make:mcp-resource ResourceName
  • Create resource templates: php artisan make:mcp-resource-template TemplateName
  • Create prompts: php artisan make:mcp-prompt PromptName
  • Create notifications: php artisan make:mcp-notification HandlerName --method=notifications/method
  • Generate from OpenAPI: php artisan make:swagger-mcp-tool <spec-url-or-file>
  • Export tools to OpenAPI: php artisan mcp:export-openapi --output=storage/api-docs-mcp/api-docs.json

Code references:

  • Tool examples: src/Services/ToolService/Examples/
  • Resource examples: src/Services/ResourceService/Examples/
  • Prompt service: src/Services/PromptService/
  • Notification handlers: src/Server/Notification/
  • Route builder: src/Routing/McpRouteBuilder.php

Swagger/OpenAPI -> MCP Tool

Generate MCP tools from a Swagger/OpenAPI spec:

# From URL
php artisan make:swagger-mcp-tool https://api.example.com/openapi.json

# From local file
php artisan make:swagger-mcp-tool ./specs/openapi.json

Useful options:

php artisan make:swagger-mcp-tool ./specs/openapi.json \
 --group-by=tag \
 --prefix=Billing \
 --test-api
  • --group-by: tag, path, or none
  • --prefix: class-name prefix for generated tools/resources
  • --test-api: test endpoint connectivity before generation

Generation behavior:

  • In interactive mode, you can choose Tool or Resource per endpoint.
  • In non-interactive mode, GET endpoints are generated as Resources and other methods as Tools.

Enhanced Interactive Preview

If you run the command without --group-by, the generator shows an interactive preview of folder structure and file counts before creation.

php artisan make:swagger-mcp-tool ./specs/openapi.json

Example preview output:

Choose how to organize your generated tools and resources:

Tag-based grouping (organize by OpenAPI tags)
 Total: 25 endpoints -> 15 tools + 10 resources
 Examples: Tools/Pet, Tools/Store, Tools/User

Path-based grouping (organize by API path)
 Total: 25 endpoints -> 15 tools + 10 resources
 Examples: Tools/Api, Tools/Users, Tools/Orders

No grouping (everything in root folder)
 Total: 25 endpoints -> 15 tools + 10 resources
 Examples: Tools/, Resources/

After generation, register generated tool classes on your MCP endpoint:

use Illuminate\Support\Facades\Route;

Route::mcp('/mcp')
 ->setServerInfo(
 name: 'Generated MCP Server',
 version: '2.0.0',
 )
 ->tools([
 \App\MCP\Tools\Billing\CreateInvoiceTool::class,
 \App\MCP\Tools\Billing\UpdateInvoiceTool::class,
 ]);

MCP Tools -> OpenAPI Export

Export all registered ToolInterface classes (via Route::mcp(...)->tools([...]) or ->dynamicTools(...)) to an OpenAPI JSON document using each tool's inputSchema(). Only endpoints configured with ->enabledApi() are included in this export and exposed through POST /tools/{tool_name}. Operations are grouped by endpoint name using OpenAPI tags. If multiple endpoints register the same tool name, the operation keeps first-registration behavior and merges all matching endpoint names into tags. If route registration is missing, the command auto-discovers tools under default paths: app/MCP/Tools and app/Tools.

# Default output: storage/api-docs-mcp/api-docs.json
php artisan mcp:export-openapi

# Custom output + metadata
php artisan mcp:export-openapi \
 --output=storage/app/mcp.openapi.json \
 --title="MCP Tools API" \
 --api-version=2.1.0

# Limit to one endpoint (id or path)
php artisan mcp:export-openapi --endpoint=/mcp

# Discover tools from additional directory paths
php artisan mcp:export-openapi --discover-path=app/MCP/Tools

# Existing output is overwritten by default
php artisan mcp:export-openapi

Enable Tool API route generation:

use Illuminate\Support\Facades\Route;

Route::mcp('/mcp')
 ->setServerInfo(name: 'OP.GG MCP Server', version: '2.0.0')
 ->enabledApi()
 ->tools([
 \App\MCP\Tools\GreetingTool::class,
 ]);

Swagger UI testing tip:

  • Exported operations use query parameters only (no requestBody) for simpler manual testing.
  • Required fields from each tool inputSchema().required are reflected in Swagger parameter validation.
  • Enum fields are exported with schema.enum so Swagger renders dropdown selections.
  • Array fields are exported with style=form + explode=true (repeat key format, e.g. desired_output_fields=items&desired_output_fields=runes).
  • /tools/{tool_name} argument parsing prefers query parameters over body/form payloads to avoid Swagger conflicts.
  • Enum fields without explicit default/example are auto-filled from the first enum value (or first non-null enum value).
  • String fields with descriptions like e.g., en_US, ko_KR, ja_JP auto-infer first sample value as default and example.

Example Tool Class

<?php

namespace App\MCP\Tools;

use App\Enums\Platform;
use OPGG\LaravelMcpServer\JsonSchema\JsonSchema;
use OPGG\LaravelMcpServer\Services\ToolService\ToolInterface;

class GreetingTool implements ToolInterface
{
 public function name(): string
 {
 return 'greeting-tool';
 }

 public function description(): string
 {
 return 'Return a greeting message.';
 }

 public function inputSchema(): array
 {
 return [
 'name' => JsonSchema::string()
 ->description('Developer Name')
 ->required(),
 'platform' => JsonSchema::string()
 ->enum(Platform::class)
 ->description('Client platform')
 ->compact(),
 ];
 }

 public function annotations(): array
 {
 return [
 'readOnlyHint' => true,
 'destructiveHint' => false,
 ];
 }

 public function execute(array $arguments): mixed
 {
 return [
 'message' => 'Hello '.$arguments['name'],
 ];
 }
}

JsonSchema Builder (Laravel-Style)

This package provides its own JsonSchema builder under the OPGG\LaravelMcpServer namespace. You can define tool schemas in a Laravel 12-style fluent format while keeping inputSchema(): array.

<?php

namespace App\MCP\Tools;

use App\Enums\Platform;
use OPGG\LaravelMcpServer\JsonSchema\JsonSchema;
use OPGG\LaravelMcpServer\Services\ToolService\ToolInterface;

class WeatherTool implements ToolInterface
{
 public function name(): string
 {
 return 'weather-tool';
 }

 public function description(): string
 {
 return 'Get weather by location.';
 }

 public function inputSchema(): array
 {
 return [
 'location' => JsonSchema::string()
 ->description('Location to query')
 ->required(),
 'platform' => JsonSchema::string()
 ->enum(Platform::class)
 ->description('Client platform'),
 'days' => JsonSchema::integer()
 ->min(1)
 ->max(7)
 ->default(1),
 ];
 }

 public function annotations(): array
 {
 return [];
 }

 public function execute(array $arguments): mixed
 {
 return ['ok' => true];
 }
}

Notes:

  • Existing full JSON Schema arrays are still supported.
  • enum() accepts either an array or a BackedEnum::class.
  • compact() can be chained after enum() to remove enum from emitted schema and append a compact hint to description (compact(), compact(null), compact(3), or compact('custom hint')).
  • Default compact example count is 3, and it can be overridden per endpoint via Route::mcp(...)->setConfig(compactEnumExampleCount: N).
  • When exporting (tools/list, OpenAPI), property maps are automatically normalized to JSON Schema object format.

Example Prompt Class

<?php

namespace App\MCP\Prompts;

use OPGG\LaravelMcpServer\Services\PromptService\Prompt;

class WelcomePrompt extends Prompt
{
 public string $name = 'welcome-user';

 public ?string $description = 'Generate a welcome message.';

 public array $arguments = [
 [
 'name' => 'username',
 'description' => 'User name',
 'required' => true,
 ],
 ];

 public string $text = 'Welcome, {username}!';
}

Example Resource Class

<?php

namespace App\MCP\Resources;

use OPGG\LaravelMcpServer\Services\ResourceService\Resource;

class BuildInfoResource extends Resource
{
 public string $uri = 'app://build-info';

 public string $name = 'Build Info';

 public ?string $mimeType = 'application/json';

 public function read(): array
 {
 return [
 'uri' => $this->uri,
 'mimeType' => $this->mimeType,
 'text' => json_encode([
 'version' => '2.0.0',
 'environment' => app()->environment(),
 ], JSON_THROW_ON_ERROR),
 ];
 }
}

Register Examples on a Route

use App\MCP\Prompts\WelcomePrompt;
use App\MCP\Resources\BuildInfoResource;
use App\MCP\Tools\GreetingTool;
use Illuminate\Support\Facades\Route;

Route::mcp('/mcp')
 ->setServerInfo(
 name: 'Example MCP Server',
 version: '2.0.0',
 )
 ->tools([GreetingTool::class])
 ->resources([BuildInfoResource::class])
 ->prompts([WelcomePrompt::class]);

Testing and Quality Commands

vendor/bin/pest
vendor/bin/phpstan analyse
vendor/bin/pint

Translation

pip install -r scripts/requirements.txt
export ANTHROPIC_API_KEY='your-api-key'
python scripts/translate_readme.py

Translate selected languages:

python scripts/translate_readme.py es ko

License

This project is distributed under the MIT license.