Introduction
The Model Context Protocol (MCP) is an open protocol that enables seamless integration between AI assistants and external data sources. In this tutorial, we'll build a complete task management system using Laravel MCP, demonstrating how to create tools, resources, and prompts that AI assistants can use to interact with your application.
What is MCP?
MCP (Model Context Protocol) allows AI assistants like Claude to interact with your application through three main components:
🛠️ Tools (Actions)
Tools are executable actions that modify data. Think of them as API endpoints that perform operations:
- Purpose: Create, update, delete operations
- Example: Creating a task, marking it complete, deleting a record
-
Characteristics:
- Can modify data
- Require validation
- Return confirmation messages
- May have side effects
📚 Resources (Read-Only Data)
Resources provide read-only access to data. They're like GET endpoints that never modify anything:
- Purpose: Fetch and display information
- Example: View statistics, list items, show reports
-
Characteristics:
- Never modify data
- Can be cached
- Fast and safe to call repeatedly
- Perfect for dashboards and reports
📝 Prompts (Workflows)
Prompts are pre-configured templates that guide AI through complex multi-step operations:
- Purpose: Combine multiple tools/resources into workflows
- Example: Generate a productivity report using stats + recent tasks + analysis
-
Characteristics:
- Provide instructions to AI
- Can chain multiple operations
- Include context and formatting guidelines
- Help maintain consistency
Think of it as an API specifically designed for AI assistants, with built-in authentication, validation, and structured responses.
What We'll Build
We're creating a task management system with:
- ✅ Create tasks with title, description, and priority
- ✅ Complete tasks with timestamp tracking
- ✅ Search tasks by keyword, priority, and completion status
- ✅ View statistics through resources
- ✅ User isolation - each user sees only their tasks
Prerequisites
- PHP 8.2+
- Composer
- Laravel 12
- Basic understanding of Laravel
Installation
First, create a new Laravel project and install Laravel MCP:
composer create-project laravel/laravel laravel-mcp
cd laravel-mcp
composer require laravel/mcp
Database Setup
Migration
Create a tasks table with user association:
// database/migrations/xxxx_create_tasks_table.php
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('description')->nullable();
$table->enum('priority', ['low', 'medium', 'high'])->default('medium');
$table->boolean('completed')->default(false);
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Task Model
Create a model with a custom query builder for advanced filtering:
// app/Models/Task.php
namespace App\Models;
use App\Models\Builders\TaskBuilder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model
{
use HasFactory;
protected $fillable = [
'title',
'description',
'priority',
'completed',
'completed_at',
'user_id'
];
protected $casts = [
'completed' => 'boolean',
'completed_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function newEloquentBuilder($query): TaskBuilder
{
return new TaskBuilder($query);
}
public function markCompleted(): void
{
$this->update([
'completed' => true,
'completed_at' => now(),
]);
}
}
Custom Query Builder
Create a custom builder for chainable query methods:
// app/Models/Builders/TaskBuilder.php
namespace App\Models\Builders;
use Illuminate\Database\Eloquent\Builder;
class TaskBuilder extends Builder
{
public function forUser(int $userId): self
{
return $this->where('user_id', $userId);
}
public function incomplete(): self
{
return $this->where('completed', false);
}
public function completed(): self
{
return $this->where('completed', true);
}
public function highPriority(): self
{
return $this->where('priority', 'high');
}
public function search(?string $keyword): self
{
if (empty($keyword)) {
return $this;
}
return $this->where(function ($query) use ($keyword) {
$query->where('title', 'like', "%{$keyword}%")
->orWhere('description', 'like', "%{$keyword}%");
});
}
public function recentlyCompleted(int $days = 30): self
{
return $this->completed()
->where('completed_at', '>=', now()->subDays($days));
}
}
Creating MCP Tools
Tools allow AI assistants to perform actions. Let's create three essential tools.
1. CreateTaskTool
// app/Mcp/Tools/CreateTaskTool.php
namespace App\Mcp\Tools;
use App\Models\Task;
use Illuminate\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
class CreateTaskTool extends Tool
{
protected string $description = 'Creates a new task with title, optional description, and priority level.';
public function handle(Request $request): Response
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'priority' => ['sometimes', 'in:low,medium,high'],
], [
'title.required' => 'You must provide a task title.',
'title.max' => 'Task title is too long (max 255 characters).',
'description.max' => 'Description is too long (max 1000 characters).',
'priority.in' => 'Priority must be: low, medium, or high.',
]);
// Associate with authenticated user
$validated['user_id'] = $request->user()->id;
$validated['priority'] = $validated['priority'] ?? 'medium';
$task = Task::create($validated);
return Response::text(
"✅ Task created successfully!\n\n" .
"**{$task->title}**\n" .
($task->description ? "{$task->description}\n" : '') .
"Priority: {$task->priority}\n" .
"ID: {$task->id}"
);
}
public function schema(JsonSchema $schema): array
{
return [
'title' => $schema->string()
->description('The title of the task')
->required(),
'description' => $schema->string()
->description('Optional detailed description'),
'priority' => $schema->enum(['low', 'medium', 'high'])
->description('Priority level')
->default('medium'),
];
}
}
2. CompleteTaskTool
// app/Mcp/Tools/CompleteTaskTool.php
namespace App\Mcp\Tools;
use App\Models\Task;
use Illuminate\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
class CompleteTaskTool extends Tool
{
protected string $description = 'Marks a task as completed.';
public function handle(Request $request): Response
{
$validated = $request->validate([
'task_id' => ['required', 'integer', 'exists:tasks,id'],
]);
$task = Task::where('id', $validated['task_id'])
->where('user_id', $request->user()->id)
->firstOrFail();
if ($task->completed) {
return Response::text(
"ℹ️ Task '{$task->title}' is already completed.\n" .
"Completed at: {$task->completed_at->format('Y-m-d H:i:s')}"
);
}
$task->markCompleted();
return Response::text(
"✅ Task completed!\n\n" .
"**{$task->title}**\n" .
"Completed at: {$task->completed_at->format('Y-m-d H:i:s')}"
);
}
public function schema(JsonSchema $schema): array
{
return [
'task_id' => $schema->integer()
->description('The ID of the task to complete')
->required(),
];
}
}
3. SearchTasksTool
// app/Mcp/Tools/SearchTasksTool.php
namespace App\Mcp\Tools;
use App\Models\Task;
use Illuminate\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
class SearchTasksTool extends Tool
{
protected string $description = 'Search and filter tasks by various criteria.';
public function handle(Request $request): Response
{
$validated = $request->validate([
'keyword' => ['nullable', 'string', 'max:255'],
'priority' => ['nullable', 'in:low,medium,high'],
'completed' => ['nullable', 'boolean'],
]);
$query = Task::forUser($request->user()->id);
if (isset($validated['keyword'])) {
$query->search($validated['keyword']);
}
if (isset($validated['priority'])) {
$query->where('priority', $validated['priority']);
}
if (isset($validated['completed'])) {
$validated['completed']
? $query->completed()
: $query->incomplete();
}
$tasks = $query->latest()->get();
if ($tasks->isEmpty()) {
return Response::text('No tasks found matching your criteria.');
}
$output = "Found {$tasks->count()} task(s):\n\n";
foreach ($tasks as $task) {
$status = $task->completed ? '✅' : '⏳';
$output .= "{$status} **{$task->title}**\n";
$output .= " Priority: {$task->priority}\n";
if ($task->description) {
$output .= " {$task->description}\n";
}
$output .= " ID: {$task->id}\n\n";
}
return Response::text($output);
}
public function schema(JsonSchema $schema): array
{
return [
'keyword' => $schema->string()
->description('Search keyword for title or description'),
'priority' => $schema->enum(['low', 'medium', 'high'])
->description('Filter by priority level'),
'completed' => $schema->boolean()
->description('Filter by completion status'),
];
}
}
Creating MCP Resources
Resources provide read-only access to data. They're perfect for statistics, reports, or any data that AI assistants need to read but not modify.
1. TaskStatsResource
This resource provides real-time statistics about the user's tasks:
// app/Mcp/Resources/TaskStatsResource.php
namespace App\Mcp\Resources;
use App\Models\Task;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;
class TaskStatsResource extends Resource
{
protected string $name = 'task-stats';
protected string $description = 'Provides statistics about user tasks including counts by priority and completion status.';
public function handle(Request $request): Response
{
$userId = $request->user()->id;
$stats = [
'total' => Task::forUser($userId)->count(),
'completed' => Task::forUser($userId)->completed()->count(),
'incomplete' => Task::forUser($userId)->incomplete()->count(),
'by_priority' => [
'high' => Task::forUser($userId)->where('priority', 'high')->count(),
'medium' => Task::forUser($userId)->where('priority', 'medium')->count(),
'low' => Task::forUser($userId)->where('priority', 'low')->count(),
],
'completion_rate' => Task::forUser($userId)->count() > 0
? round((Task::forUser($userId)->completed()->count() / Task::forUser($userId)->count()) * 100, 2)
: 0,
];
$output = "📊 **Task Statistics**\n\n";
$output .= "**Overview:**\n";
$output .= "• Total tasks: {$stats['total']}\n";
$output .= "• Completed: {$stats['completed']}\n";
$output .= "• Incomplete: {$stats['incomplete']}\n";
$output .= "• Completion rate: {$stats['completion_rate']}%\n\n";
$output .= "**By Priority:**\n";
$output .= "• High: {$stats['by_priority']['high']}\n";
$output .= "• Medium: {$stats['by_priority']['medium']}\n";
$output .= "• Low: {$stats['by_priority']['low']}\n";
return Response::text($output);
}
}
2. RecentCompletedTasksResource
This resource shows recently completed tasks:
// app/Mcp/Resources/RecentCompletedTasksResource.php
namespace App\Mcp\Resources;
use App\Models\Task;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;
class RecentCompletedTasksResource extends Resource
{
protected string $name = 'recent-completed-tasks';
protected string $description = 'Lists tasks completed in the last 30 days with completion timestamps.';
public function handle(Request $request): Response
{
$tasks = Task::forUser($request->user()->id)
->recentlyCompleted(30)
->orderBy('completed_at', 'desc')
->limit(10)
->get();
if ($tasks->isEmpty()) {
return Response::text('No tasks completed in the last 30 days.');
}
$output = "✅ **Recently Completed Tasks** (Last 30 days)\n\n";
foreach ($tasks as $task) {
$daysAgo = $task->completed_at->diffForHumans();
$output .= "**{$task->title}**\n";
$output .= " Priority: {$task->priority}\n";
$output .= " Completed: {$daysAgo}\n";
$output .= " Date: {$task->completed_at->format('Y-m-d H:i')}\n\n";
}
return Response::text($output);
}
}
Creating MCP Prompts
Prompts are pre-configured templates that help AI assistants perform common workflows. They combine multiple tools and provide context.
ProductivityReportPrompt
This prompt generates a comprehensive productivity report:
// app/Mcp/Prompts/ProductivityReportPrompt.php
namespace App\Mcp\Prompts;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;
class ProductivityReportPrompt extends Prompt
{
protected string $name = 'productivity-report';
protected string $description = 'Generates a comprehensive productivity report with task statistics, recent completions, and recommendations.';
public function handle(Request $request): Response
{
$instructions = <<<'TEXT'
Generate a productivity report by:
1. **Fetch Statistics**: Use the 'task-stats' resource to get overall task metrics
2. **Recent Activity**: Use the 'recent-completed-tasks' resource to see what's been done
3. **Current Tasks**: Use SearchTasksTool with completed=false to see pending work
4. **Analysis**: Provide insights on:
- Completion rate trends
- Priority distribution
- Workload balance
- Recommendations for improvement
Format the report with:
- Executive summary at the top
- Key metrics with visual indicators (📊, ✅, ⏳)
- Actionable recommendations
- Encouraging tone
Example structure:
# 📈 Your Productivity Report
## Executive Summary
[Brief overview of performance]
## Key Metrics
[Statistics from resources]
## Recent Achievements
[Completed tasks]
## Current Focus
[Pending high-priority tasks]
## Recommendations
[Actionable suggestions]
TEXT;
return Response::text($instructions);
}
public function arguments(): array
{
return [
// No arguments needed - uses current user context
];
}
}
Creating the MCP Server
Register all your tools, resources, and prompts in a server class:
// app/Mcp/Servers/TaskServer.php
namespace App\Mcp\Servers;
use App\Mcp\Tools\CreateTaskTool;
use App\Mcp\Tools\CompleteTaskTool;
use App\Mcp\Tools\SearchTasksTool;
use App\Mcp\Resources\TaskStatsResource;
use App\Mcp\Resources\RecentCompletedTasksResource;
use App\Mcp\Prompts\ProductivityReportPrompt;
use Laravel\Mcp\Server;
class TaskServer extends Server
{
protected string $name = 'Task Management Server';
protected string $version = '1.0.0';
protected string $instructions = <<<'MARKDOWN'
# Task Management Server
A comprehensive task management system with AI-powered features.
## 🛠️ Tools (Actions)
Use these to modify data:
- **create-task-tool**: Create new tasks with title, description, and priority
- **complete-task-tool**: Mark tasks as completed with timestamp
- **search-tasks-tool**: Find and filter tasks by various criteria
## 📚 Resources (Read-Only Data)
Use these to access information:
- **task-stats**: View statistics and metrics about tasks
- **recent-completed-tasks**: See recently completed tasks (last 30 days)
## 📝 Prompts (Workflows)
Use these for complex operations:
- **productivity-report**: Generate comprehensive productivity analysis
## 🔒 Security
All operations are user-specific. Users can only access their own tasks.
MARKDOWN;
protected array $tools = [
CreateTaskTool::class,
CompleteTaskTool::class,
SearchTasksTool::class,
];
protected array $resources = [
TaskStatsResource::class,
RecentCompletedTasksResource::class,
];
protected array $prompts = [
ProductivityReportPrompt::class,
];
}
Routing
Register your MCP server in the routes:
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;
use App\Mcp\Servers\TaskServer;
Mcp::web('/mcp/tasks', TaskServer::class);
Authentication
Laravel MCP supports Sanctum authentication. Generate tokens for users:
// app/Console/Commands/CreateMcpToken.php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class CreateMcpToken extends Command
{
protected $signature = 'mcp:token {email}';
protected $description = 'Create an MCP access token for a user';
public function handle()
{
$user = User::where('email', $this->argument('email'))->first();
if (!$user) {
$this->error('User not found!');
return 1;
}
$token = $user->createToken('mcp-access')->plainTextToken;
$this->info("Token created successfully:");
$this->line($token);
return 0;
}
}
Generate a token:
php artisan mcp:token user@example.com
Understanding the Differences
Here's a quick comparison to help you choose the right component:
Component | Modifies Data? | Use Case | Example |
---|---|---|---|
Tool | ✅ Yes | Actions that change state | Create task, update status |
Resource | ❌ No | Read-only information | View stats, list items |
Prompt | ⚙️ Orchestrates | Multi-step workflows | Generate report, analyze data |
When to Use Each
Use a Tool when:
- You need to create, update, or delete data
- The operation has side effects
- You need validation and error handling
- Example:
CreateTaskTool
,CompleteTaskTool
Use a Resource when:
- You need to display information
- The data is read-only
- You want to provide context to AI
- Example:
TaskStatsResource
,RecentTasksResource
Use a Prompt when:
- You need to guide AI through multiple steps
- You want consistent output formatting
- You're combining multiple tools/resources
- Example:
ProductivityReportPrompt
,WeeklyReviewPrompt
Testing
Laravel MCP provides excellent testing utilities for all three components:
// tests/Feature/TaskMcpTest.php
use App\Mcp\Servers\TaskServer;
use App\Mcp\Tools\CreateTaskTool;
use App\Models\User;
test('can create a task', function () {
$user = User::factory()->create();
$response = TaskServer::actingAs($user)
->tool(CreateTaskTool::class, [
'title' => 'Write tutorial',
'description' => 'Complete Laravel MCP tutorial',
'priority' => 'high',
]);
$response->assertOk();
$response->assertSee('Write tutorial');
$response->assertSee('high');
expect(Task::first())
->user_id->toBe($user->id)
->title->toBe('Write tutorial')
->priority->toBe('high');
});
test('users only see their own tasks', function () {
$user1 = User::factory()->create();
$user2 = User::factory()->create();
Task::factory()->create([
'user_id' => $user1->id,
'title' => 'User 1 task',
]);
Task::factory()->create([
'user_id' => $user2->id,
'title' => 'User 2 task',
]);
$response = TaskServer::actingAs($user1)
->tool(SearchTasksTool::class, []);
$response->assertOk();
$response->assertSee('User 1 task');
$response->assertDontSee('User 2 task');
});
test('can access task statistics resource', function () {
$user = User::factory()->create();
Task::factory()->count(5)->create(['user_id' => $user->id]);
Task::factory()->completed()->count(3)->create(['user_id' => $user->id]);
$response = TaskServer::actingAs($user)
->resource(TaskStatsResource::class);
$response->assertOk();
$response->assertSee('Total tasks: 8');
$response->assertSee('Completed: 3');
$response->assertSee('Incomplete: 5');
});
test('can use productivity report prompt', function () {
$user = User::factory()->create();
$response = TaskServer::actingAs($user)
->prompt(ProductivityReportPrompt::class);
$response->assertOk();
$response->assertSee('productivity report');
$response->assertSee('task-stats');
$response->assertSee('recent-completed-tasks');
});
Run tests:
php artisan test
Advanced Features
Custom Query Builder
The TaskBuilder
class demonstrates the power of Laravel's query builder pattern:
// Chainable queries
$tasks = Task::forUser($userId)
->incomplete()
->highPriority()
->search('important')
->get();
Validation with Custom Messages
MCP integrates seamlessly with Laravel's validation:
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
], [
'title.required' => 'Custom error message for AI',
]);
User Isolation
Every query automatically filters by the authenticated user:
$task = Task::where('id', $taskId)
->where('user_id', $request->user()->id)
->firstOrFail();
Deployment
Environment Configuration
APP_URL=https://your-domain.com
MCP_ENABLED=true
SANCTUM_STATEFUL_DOMAINS=your-domain.com
Production Checklist
- ✅ Enable HTTPS
- ✅ Configure CORS properly
- ✅ Set up rate limiting
- ✅ Use queue workers for heavy operations
- ✅ Monitor API usage
- ✅ Implement proper logging
Best Practices
1. Clear Tool Descriptions
protected string $description = 'Creates a new task with title, optional description, and priority level.';
AI assistants use these descriptions to understand when to use your tools.
2. Comprehensive Schemas
public function schema(JsonSchema $schema): array
{
return [
'title' => $schema->string()
->description('The title of the task')
->required(),
];
}
Detailed schemas help AI understand parameter requirements.
3. User-Friendly Responses
return Response::text(
"✅ Task created successfully!\n\n" .
"**{$task->title}**\n" .
"Priority: {$task->priority}"
);
Format responses for readability by both AI and humans.
4. Proper Error Handling
if ($task->completed) {
return Response::text(
"ℹ️ Task is already completed.\n" .
"Completed at: {$task->completed_at->format('Y-m-d H:i:s')}"
);
}
Provide helpful feedback for edge cases.
Common Pitfalls
1. Forgetting User Association
❌ Wrong:
$task = Task::find($taskId);
✅ Correct:
$task = Task::where('id', $taskId)
->where('user_id', $request->user()->id)
->firstOrFail();
2. Missing Validation
Always validate input from AI assistants:
$validated = $request->validate([
'task_id' => ['required', 'integer', 'exists:tasks,id'],
]);
3. Unclear Responses
Make responses clear and actionable:
// ❌ Bad
return Response::text('Done');
// ✅ Good
return Response::text(
"✅ Task '{$task->title}' completed successfully!\n" .
"Completed at: {$task->completed_at->format('Y-m-d H:i:s')}"
);
Conclusion
Laravel MCP provides a powerful framework for building AI-accessible APIs. Key takeaways:
- Tools enable actions (create, update, delete)
- Validation ensures data integrity
- User isolation maintains security
- Testing utilities make development smooth
- Custom builders enable complex queries
The complete code is available on GitHub.
Real-World Usage Examples
Example 1: AI Assistant Creating a Task
User: "Create a high priority task to review Q4 budget"
AI uses CreateTaskTool:
{
"title": "Review Q4 budget",
"priority": "high"
}
Response: ✅ Task created successfully!
**Review Q4 budget**
Priority: high
ID: 42
Example 2: AI Checking Statistics
User: "How am I doing with my tasks?"
AI accesses TaskStatsResource:
Response: 📊 Task Statistics
Total tasks: 25
Completed: 18
Completion rate: 72%
Example 3: AI Generating Report
User: "Give me a productivity report"
AI uses ProductivityReportPrompt which:
1. Fetches stats from TaskStatsResource
2. Gets recent completions from RecentCompletedTasksResource
3. Searches incomplete high-priority tasks
4. Generates formatted report with insights
Architecture Diagram
┌─────────────────────────────────────────┐
│ AI Assistant (Claude) │
└────────────────┬────────────────────────┘
│ MCP Protocol
│
┌────────────────▼────────────────────────┐
│ Laravel MCP Server │
│ ┌─────────────────────────────────┐ │
│ │ TaskServer │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Tools │ │Resources │ │ │
│ │ │ - Create│ │ - Stats │ │ │
│ │ │ - Update│ │ - Recent│ │ │
│ │ │ - Search│ │ │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ │ │
│ │ │ Prompts │ │ │
│ │ │ - Report│ │ │
│ │ └──────────┘ │ │
│ └─────────────────────────────────┘ │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ Laravel Application │
│ ┌──────────┐ ┌──────────┐ │
│ │ Models │ │ Database │ │
│ │ - Task │ │ - tasks │ │
│ │ - User │ │ - users │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────┘
Next Steps
- Add more Resources (task trends, team statistics)
- Create additional Prompts (weekly review, goal setting)
- Add file attachments to tasks
- Implement task sharing between users
- Add real-time notifications
- Create custom task categories
- Implement recurring tasks
Top comments (0)