MCP Server
Laravel Restify provides seamless integration with the Model Context Protocol (MCP), allowing AI agents to interact with your REST API resources through structured tool interfaces. So you can simply tranform your repositories into a tools for AI agents to consume. Incredible!
Setup & Registration
Basic Server Registration
Register the MCP server in your application's service provider or routes file:
use Binaryk\LaravelRestify\MCP\RestifyServer;
use Laravel\Mcp\Facades\Mcp;
// Register the MCP server
Mcp::web('restify', RestifyServer::class)->name('mcp.restify');
This creates an MCP server endpoint at /mcp/restify that AI agents can connect to.
Adding Authentication & Middleware
For production applications, you'll want to add authentication and other middleware:
use Binaryk\LaravelRestify\MCP\RestifyServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('restify', RestifyServer::class)->middleware([
'auth:sanctum',
])->name('mcp.restify');
And that's it! Now you can access your Restify API through the MCP server with authentication. Go into n8n or your AI agent of choice and connect to the MCP server endpoint.
Terminal/STDIN Access (Local MCP)
For terminal-based AI agents (like Claude Desktop, cursor, or other CLI tools that support MCP), you can expose your Restify API through STDIN/STDOUT using the local syntax. This allows direct integration without HTTP overhead.
Registering a Local MCP Server
Register your local MCP server in the routes/ai.php file:
use Laravel\Mcp\Facades\Mcp;
use App\Mcp\Servers\GroweeStdServer;
// Register for terminal/STDIN access
Mcp::local('growee', GroweeStdServer::class);
Creating a Terminal-Accessible Server with Authentication
When using terminal access, authentication must be handled within the server's boot() method since there's no HTTP middleware pipeline. Here's a complete example that extends RestifyServer and implements Sanctum authentication:
<?php
namespace App\Mcp\Servers;
use Binaryk\LaravelRestify\MCP\RestifyServer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Mcp\Server\Exceptions\McpException;
use Laravel\Sanctum\PersonalAccessToken;
class GroweeStdServer extends RestifyServer
{
public function boot(): void
{
$request = request();
// Get the API token from Authorization header
$bearerToken = $request->bearerToken();
// Fail if no API key is provided
if (! $bearerToken) {
throw new McpException('API key is required. Please provide a Bearer token in the Authorization header.');
}
// Try to authenticate using Sanctum
$token = PersonalAccessToken::findToken($bearerToken);
if (! $token) {
throw new McpException('Invalid API key provided. Please check your Bearer token.');
}
// Verify the token is active
if (! $token->tokenable) {
throw new McpException('API token is not associated with a valid user.');
}
// Set the authenticated user on both sanctum and default guard
Auth::guard('sanctum')->setUser($token->tokenable);
Auth::setUser($token->tokenable);
// Set the user resolver for the request
$request->setUserResolver(function () use ($token) {
return $token->tokenable;
});
// Call parent boot to discover tools, resources, and prompts
parent::boot();
}
}
Key Differences from Web Access
- No Middleware Pipeline: Unlike
Mcp::web(), thelocalsyntax doesn't support middleware. All authentication and authorization must be implemented in theboot()method. - Direct API Token: Terminal clients provide Bearer tokens directly through the Authorization header.
- Request Context: Access to the request is available via the
request()helper function. - Error Handling: Use
McpExceptionfor authentication failures to provide clear error messages to the terminal client. - Registration Location: Local MCP servers are typically registered in
routes/ai.phpinstead of web routes.
Client Configuration
Terminal-based AI clients (like Claude Desktop) can connect to your local MCP server by configuring the connection with your API token. The exact configuration depends on your client, but typically involves:
- Setting the Authorization header with your Sanctum token
- Specifying the server name (e.g., 'growee')
- Pointing to your Laravel application's MCP endpoint
Security Considerations
- Token Security: API tokens are passed via the Authorization header and should be kept secure
- Token Scopes: Consider implementing token abilities/scopes to limit access
- Rate Limiting: Implement rate limiting at the application level since middleware isn't available
- Audit Logging: Log authentication attempts and API usage for security monitoring
- Token Rotation: Implement token expiration and rotation policies
🔒 Security Best Practices
- Apply field visibility controls (
hideFromMcp()) for sensitive data - Audit MCP field access patterns
- Implement rate limiting for token-heavy operations
Common Issues
Schema Validation Error
Error: [ERROR: Received tool input did not match expected schema]
Cause: This occurs when the field type is not identified correctly by the MCP server, leading to schema mismatches between what the AI agent sends and what Laravel Restify expects.
Solution: You need to explicitly override the field type for the MCP schema using the toolSchema() method:
field('project_id')
->toolSchema(function(\Laravel\Mcp\Server\Tools\ToolInputSchema $schema) {
$schema->string('project_id')
->description('The ID of the project associated with the timesheet entry.')
->required();
}),
This approach allows you to:
- Explicitly define the expected data type (string, integer, boolean, etc.)
- Add detailed descriptions for AI agents
- Set validation rules (required, optional)
- Override automatic type inference when it's incorrect
Error Responses
When an operation fails, Restify returns a real MCP error result with isError set to true on the tool result, so agents can reliably distinguish failures from successful calls.
The error content is a JSON payload with a stable shape. The keys are guaranteed to be stable so agents can parse them:
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"errors": {
"title": ["The title field is required."]
}
}
error: a human-readable message.code: a machine-readable code such asMISSING_PARAMETER,INVALID_OPERATION,VALIDATION_ERROR,AUTHORIZATION_ERROR,NOT_FOUND, orEXECUTION_ERROR.errors: present for validation failures, containing per-field messages. Actions and getters surface the same per-field detail as CRUDstore/updateoperations.detail: present only for unexpected server errors and only whenapp.debugis enabled. Internal exception messages are never leaked to clients in production.
Structured Tool Output
Successful tool results are emitted with both the existing JSON text content and a machine-parsable structuredContent field on the same tool result. This lets hosts read the payload deterministically without re-parsing the text block, while clients that only read text continue to work unchanged.
Both the operation tools (index, show, store, update, delete, profile, action, getter) and the 4 wrapper tools (discover-repositories, get-repository-operations, get-operation-details, execute-operation) return structured content. The structuredContent value is always byte-for-byte identical to the JSON parsed from the text block:
{
"result": {
"content": [
{ "type": "text", "text": "{ \"success\": true, \"total\": 1, ... }" }
],
"structuredContent": { "success": true, "total": 1, "...": "..." },
"isError": false
}
}
The three read-only wrapper discovery tools also publish an outputSchema (their shape is stable), so hosts can validate structuredContent up front. Operation tools such as index/show derive their shape from repository fields at runtime and therefore do not declare a static outputSchema.
Error results are intentionally not given structuredContent: they are already flagged with isError: true and carry the stable JSON error payload described above in their text content.
Configuration
The MCP integration respects your existing Restify configuration and adds MCP-specific options:
'mcp' => [
'enabled' => true,
'server_name' => 'My App MCP Server',
'server_version' => '1.0.0',
'default_pagination' => 25,
'mode' => env('RESTIFY_MCP_MODE', 'wrapper'), // 'wrapper' (default) or 'direct'
'read_only' => env('RESTIFY_MCP_READ_ONLY', false), // refuse all write operations
'files' => [
'allow_local_paths' => env('RESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS', false),
'max_bytes' => env('RESTIFY_MCP_FILES_MAX_BYTES', 10 * 1024 * 1024),
'timeout' => env('RESTIFY_MCP_FILES_TIMEOUT', 10),
],
'tools' => [
'exclude' => [
// Tools to exclude from discovery
],
'include' => [
// Additional tools to include
],
],
],
As of Restify 10, the MCP mode defaults to wrapper (previously direct). Wrapper mode exposes only 4 tools and hides individual operations behind a progressive discovery workflow. Set RESTIFY_MCP_MODE=direct to restore the previous behavior.
MCP Mode: Direct vs Wrapper
Laravel Restify offers two modes for exposing your repositories through MCP: Direct Mode and Wrapper Mode. Each mode has different trade-offs in terms of token usage and discoverability.
Direct Mode
In direct mode, every repository operation (index, show, store, update, delete) and custom action/getter is exposed as a separate MCP tool. This provides maximum discoverability for AI agents.
When to use Direct Mode:
- You have a small number of repositories (< 10)
- You want AI agents to instantly see all available operations
- Token usage is not a concern
- You prefer simpler, more straightforward tool discovery
Example: With 10 repositories, each having 5 CRUD operations plus 2 actions, you would expose 70 tools to the AI agent.
Configuration:
// .env
RESTIFY_MCP_MODE=direct
Wrapper Mode (Default, Token-Efficient)
Wrapper mode is the default. It uses a progressive discovery pattern that exposes only 4 wrapper tools regardless of how many repositories you have. AI agents discover and execute operations through a multi-step workflow.
When to use Wrapper Mode:
- You have many repositories (10+)
- Token usage efficiency is important (e.g., working with large context windows)
- You want to reduce the initial tool list size
- You're building complex applications with dozens of repositories
Token Savings Example:
- Direct mode with 50 repositories: ~250+ tools exposed
- Wrapper mode with 50 repositories: 4 tools exposed
- Token reduction: ~98% fewer tokens used for tool definitions
Configuration:
// .env
RESTIFY_MCP_MODE=wrapper
The 4 Wrapper Tools
When using wrapper mode, AI agents use these 4 tools in a progressive discovery workflow:
1. discover-repositories
Lists all available MCP-enabled repositories with metadata. Supports optional search filtering.
Example Request:
{
"search": "user"
}
Example Response:
{
"success": true,
"repositories": [
{
"name": "users",
"title": "Users",
"description": "Manage user accounts",
"operations": ["index", "show", "store", "update", "delete", "profile"],
"actions_count": 2,
"getters_count": 1
}
]
}
2. get-repository-operations
Lists all operations, actions, and getters available for a specific repository.
Example Request:
{
"repository": "users"
}
Example Response:
{
"success": true,
"repository": "users",
"operations": [
{
"type": "index",
"name": "users-index-tool",
"title": "Users",
"description": "List User records",
"annotations": { "readOnlyHint": true }
},
{
"type": "delete",
"name": "users-delete-tool",
"title": "Users Delete",
"description": "Delete an existing User record",
"annotations": { "destructiveHint": true, "idempotentHint": true }
}
],
"actions": [
{
"name": "activate-user",
"title": "Activate User",
"description": "Activate a user account",
"annotations": { "openWorldHint": true }
}
],
"getters": [
{
"name": "active-users",
"title": "Active Users",
"description": "Get all active users",
"annotations": { "readOnlyHint": true }
}
]
}
Each operation, action, and getter carries an annotations object with the same MCP tool safety hints that direct mode exposes on the tool definitions, so agents get consistent safety signals through the wrapper path.
3. get-operation-details
Returns the complete JSON schema and documentation for a specific operation, including all parameters, validation rules, and examples.
Example Request:
{
"repository": "users",
"operation_type": "store"
}
Example Response:
{
"success": true,
"operation": "store",
"type": "create",
"title": "Create User",
"description": "Create a new user account",
"annotations": {},
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The user's full name",
"required": true
},
"email": {
"type": "string",
"description": "The user's email address",
"required": true
}
}
},
"examples": [
{
"name": "John Doe",
"email": "[email protected]"
}
]
}
4. execute-operation
Executes a repository operation with the provided parameters. This is the final step after discovering the repository, listing operations, and getting operation details.
Example Request:
{
"repository": "users",
"operation_type": "store",
"parameters": {
"name": "John Doe",
"email": "[email protected]"
}
}
Example Response:
{
"success": true,
"data": {
"id": 123,
"name": "John Doe",
"email": "[email protected]"
}
}
Tool Safety Annotations
Every operation tool is tagged with the MCP tool annotations that describe its behavior, so a host can warn before running a destructive call or safely cache a read-only one. Restify applies them automatically based on the operation type:
| Operation | Annotation hints |
|---|---|
index, show, profile, getters, global-search | readOnlyHint: true |
store | (none — a create is neither idempotent nor read-only) |
update | idempotentHint: true |
delete | destructiveHint: true, idempotentHint: true |
| actions | openWorldHint: true (an action may touch external systems) |
In direct mode these appear in the annotations field of each tool definition returned by tools/list. In wrapper mode the per-operation tools are not registered with the host, so the same hints are surfaced inside the annotations object of every entry returned by get-repository-operations and get-operation-details.
Wrapper Mode Workflow
Here's a typical workflow when an AI agent uses wrapper mode:
- Discover repositories: Agent calls
discover-repositoriesto see what repositories are available - Explore operations: Agent calls
get-repository-operationsfor the target repository - Get schema: Agent calls
get-operation-detailsto understand required parameters - Execute: Agent calls
execute-operationwith the correct parameters
This progressive discovery pattern reduces token usage while maintaining full functionality.
Switching Between Modes
You can switch between modes at any time by updating your .env file:
# Wrapper mode (default, token-efficient)
RESTIFY_MCP_MODE=wrapper
# Direct mode
RESTIFY_MCP_MODE=direct
No code changes are required. The MCP server automatically adapts to the configured mode. The mode is read from configuration only — a request parameter cannot override it.
Performance Considerations
Direct Mode:
- ✅ Faster initial discovery (all tools visible immediately)
- ❌ Higher token usage (all tools loaded into context)
- ✅ Simpler for AI agents to understand
- ❌ Can overwhelm context window with large applications
Wrapper Mode:
- ✅ Dramatically lower token usage (4 tools vs 100+)
- ✅ Scales well with large applications
- ❌ Requires multi-step workflow
- ✅ Better for applications with many repositories
Best Practices
- Keep the default Wrapper Mode in production if you have 10+ repositories or token efficiency is important
- Switch to Direct Mode during development when you want every tool visible immediately
- Use wrapper mode when working with AI agents that have limited context windows
- Monitor token usage to determine which mode is best for your application
- Document your choice so team members understand which mode is active
Read-Only Mode
When you want to give an AI agent access to your data without letting it mutate anything, enable read-only mode:
RESTIFY_MCP_READ_ONLY=true
With read-only mode enabled, every write operation (store, update, delete, and custom actions) is refused:
- In direct mode, only read tools (
index,show, getters, andprofile) are registered with the host. Write tools are never exposed. - In wrapper mode, write operations are hidden from
discover-repositories,get-repository-operations, andget-operation-details, andexecute-operationrejects them with an authorization-styleisErrorresponse.
Enforcement happens centrally, so an agent cannot re-enable a hidden operation by calling it directly.
Generating an Agent Skill
Agents increasingly consume APIs through Agent Skills and code execution rather than raw MCP calls. Restify can emit a ready-to-ship skill straight from your MCP-enabled repositories, so the documentation an agent reads never drifts from what your API actually exposes:
php artisan restify:skill
By default the files are written to ./restify-skill/. Pass --path to choose another directory:
php artisan restify:skill --path=resources/skills/api
The command produces two files:
SKILL.md— a Claude Agent Skill with YAML front matter and markdown that teaches the base URL convention (/api/restify/{repository}), Sanctum bearer authentication, pagination, sorting, related resources, search, and Restify's filter convention (filterName=value, notfilter[name]=value). It then documents one section per MCP-enabled repository: available operations, the field schema, matchables/sortables/searchables, and copy-pastecurlexamples.openapi.json— an OpenAPI 3.1 description with a path per enabled operation (index,show,store,update,delete, actions, and getters), request bodies derived from your store/update validation rules, and abearerAuthsecurity scheme.
Only repositories using the HasMcpTools trait and the operations they expose are included. Read-only mode is honored too: when it's enabled, write operations and actions are omitted from both files.
File Ingestion Safety
Repository File fields accept an AI-supplied value that Restify turns into an upload. To avoid server-side request forgery (SSRF) and local file disclosure, ingestion is locked down by default:
- Local filesystem paths are disabled. A value like
/etc/hostsis ignored unless you explicitly opt in withRESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS=true. - Only
http/httpsURLs are fetched. Other schemes (file://,ftp://, …) are rejected. - Private, loopback, link-local, and reserved IPs are rejected, including hosts that resolve to them, so an agent cannot reach internal services (for example
http://169.254.169.254orhttp://127.0.0.1). - Downloads are capped and time-limited by
RESTIFY_MCP_FILES_MAX_BYTES(default 10MB) andRESTIFY_MCP_FILES_TIMEOUT(default 10 seconds). Redirects are not followed.
# Opt in to local paths (use with care)
RESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS=true
# Tune the remote fetch guardrails
RESTIFY_MCP_FILES_MAX_BYTES=10485760
RESTIFY_MCP_FILES_TIMEOUT=10
Fine-Grained Tool Permissions
Laravel Restify's MCP integration includes a powerful permission system that allows you to control which tools each API token can access. This is essential for multi-tenant applications or when you need to restrict AI agent capabilities.
How Permission Control Works
The RestifyServer class provides a canUseTool() method that is called whenever a tool is accessed. By default, this method returns true (all tools are accessible), but you can override it in your application server to implement custom permission logic.
Key Behavior:
canUseTool()is called during tool discovery (what tools the AI agent sees)canUseTool()is called during tool execution (whether the operation is allowed)- In wrapper mode, permissions are checked for individual operations, not just the 4 wrapper tools
- Tools without permission are completely hidden from the AI agent
Implementing Token-Based Permissions
Create a custom MCP server that extends RestifyServer and implements permission checks:
<?php
namespace App\Mcp;
use Binaryk\LaravelRestify\MCP\RestifyServer;
use App\Models\McpToken;
class ApplicationServer extends RestifyServer
{
public function canUseTool(string|object $tool): bool
{
// Extract tool name from string or object
$toolName = is_string($tool) ? $tool : $tool->name();
// Get the API token from the request
$bearerToken = request()->bearerToken();
if (!$bearerToken) {
return false;
}
// Find the MCP token record
$mcpToken = McpToken::where('token', hash('sha256', $bearerToken))
->first();
if (!$mcpToken) {
return false;
}
// Check if this tool is in the token's allowed tools list
// $mcpToken->allowed_tools is a JSON array like:
// ["users-index", "posts-store", "posts-update-status-action"]
return in_array($toolName, $mcpToken->allowed_tools ?? [], true);
}
}
Then register your custom server instead of the base RestifyServer:
use App\Mcp\ApplicationServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('restify', ApplicationServer::class)->name('mcp.restify');
Generating Tokens with Tool Selection
To create a token creation UI, you need to show users which tools are available and let them select which ones to grant access to:
use Binaryk\LaravelRestify\MCP\RestifyServer;
// Get all available tools
$server = app(RestifyServer::class);
$allTools = $server->getAllAvailableTools();
// Returns a collection with all tools, regardless of mode:
// [
// ['name' => 'users-index', 'title' => 'List Users', 'description' => '...', 'category' => 'CRUD Operations'],
// ['name' => 'users-store', 'title' => 'Create User', 'description' => '...', 'category' => 'CRUD Operations'],
// ['name' => 'posts-publish-action', 'title' => 'Publish Post', 'description' => '...', 'category' => 'Actions'],
// ]
// Group tools by category for better UI
$groupedTools = $allTools->toSelectOptions();
// Returns:
// [
// ['category' => 'CRUD Operations', 'tools' => [...]],
// ['category' => 'Actions', 'tools' => [...]],
// ['category' => 'Getters', 'tools' => [...]],
// ]
Example Token Creation Flow
Here's a complete example of creating a token with specific tool permissions:
use App\Models\McpToken;
use Illuminate\Support\Str;
// 1. Show available tools to user
$server = app(RestifyServer::class);
$availableTools = $server->getAllAvailableTools()->toSelectOptions();
// 2. User selects which tools to grant access to
$selectedTools = [
'users-index',
'users-show',
'posts-index',
'posts-store',
'posts-publish-action',
];
// 3. Generate the token
$plainTextToken = Str::random(64);
// 4. Store token with permissions
$mcpToken = McpToken::create([
'name' => 'AI Agent Token',
'token' => hash('sha256', $plainTextToken),
'allowed_tools' => $selectedTools, // Cast to JSON in model
'user_id' => auth()->id(),
'expires_at' => now()->addDays(30),
]);
// 5. Return plain text token to user (only shown once)
return response()->json([
'token' => $plainTextToken,
'allowed_tools' => $selectedTools,
]);
Database Schema Example
Here's a suggested database schema for storing MCP tokens with permissions:
Schema::create('mcp_tokens', function (Blueprint $table) {
$table->id();
$table->string('name'); // Token description
$table->string('token')->unique(); // Hashed token
$table->json('allowed_tools')->nullable(); // Array of tool names
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
And the corresponding Eloquent model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class McpToken extends Model
{
protected $fillable = [
'name',
'token',
'allowed_tools',
'user_id',
'expires_at',
];
protected $casts = [
'allowed_tools' => 'array',
'expires_at' => 'datetime',
'last_used_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
Permission Behavior in Different Modes
Direct Mode:
- Tools without permission are filtered out of the tools list
- AI agent only sees tools they have access to
- Attempting to use a restricted tool results in "tool not found"
Wrapper Mode:
- The 4 wrapper tools are always visible (discover, get-operations, get-details, execute)
- When discovering repositories, only those with ≥1 accessible operation are shown
- When listing operations, only permitted operations appear
- Attempting to get details or execute a restricted operation throws
AuthorizationException
Getting Authorized Tools at Runtime
You can get only the tools the current user/token can access:
$server = app(RestifyServer::class);
$authorizedTools = $server->getAuthorizedTools();
// Returns only tools that pass the canUseTool() check
// Useful for showing "Your API Access" in a dashboard
Security Best Practices
- Always hash tokens before storing in the database (use
hash('sha256', $token)) - Generate tokens with sufficient entropy (at least 64 random characters)
- Implement token expiration and enforce it in
canUseTool() - Log token usage by updating
last_used_aton each request - Revoke tokens by deleting the database record
- Use HTTPS to protect tokens in transit
- Implement rate limiting per token to prevent abuse
- Audit permission changes when updating
allowed_tools