I Connected n8n to Laravel with MCP — Now My AI Agent Can Run Real Business Workflows

The first time an AI agent says, “Done — I paused the subscription, created the support ticket, and queued the follow-up email,” the feeling is not excitement.

It is suspicion.

Because now you have to answer the real questions:

  • Did it actually pause the subscription, or did it just say it did?
  • Did it check whether the customer was allowed to pause it?
  • Did it create duplicate tickets?
  • Did it trigger the email twice?
  • Who authorized the agent to touch production data?
  • Where is the audit log?

That is the point where a chatbot stops being a toy and becomes a business workflow problem.

This article is about connecting n8n, Laravel, and MCP in a way that lets an AI agent run real workflows without turning your application into an uncontrolled automation sandbox. The setup is practical: Laravel stays responsible for business rules, n8n handles orchestration and AI plumbing, and MCP becomes the controlled bridge between them.

TL;DR

  • Use Laravel as the source of truth for business state, permissions, and workflow execution.
  • Use n8n as the AI orchestration layer: prompts, model calls, retries, notifications, and external integrations.
  • Expose Laravel capabilities through MCP tools, not raw database access.
  • Design tools around business outcomes, not CRUD operations.
  • Give n8n a narrow service identity, not an admin token.
  • Return receipts and workflow IDs for long-running jobs.
  • Add idempotency, approvals, and audit logs before letting the agent do anything interesting.

📋 Table of Contents

1. The chatbot stops being cute when it needs to change data

A support assistant that answers documentation questions is relatively safe.

A support assistant that can do things is a different system entirely.

Scenario:
A customer asks, “My payment failed, but I already updated my card. Can you retry the invoice and make sure my team does not lose access?”

A chat-only assistant can explain what a payment failure is. It can link to billing docs. It can even sound helpful.

But the real business workflow is:

  1. Identify the customer.
  2. Check the subscription state.
  3. Check the failed invoice.
  4. Verify that the payment method was updated.
  5. Retry the invoice.
  6. Prevent duplicate retries.
  7. Notify the customer.
  8. Record what happened for support and finance.

That is not a prompt problem. That is a permissions, orchestration, and state problem.

The obvious solution is often dangerous: give n8n direct access to the database or a bunch of internal APIs and let the model figure it out. That works in a demo. In production, it creates a system where the AI can accidentally write to the wrong table, trigger workflows out of order, or perform actions that should require human review.

Solution:
Put a deliberate boundary between the AI orchestration layer and the application layer.

In the architecture I prefer:

  • Laravel owns the domain model, validation, policies, queues, and workflow records.
  • n8n owns the AI agent loop, external communication, retries, and human-notification steps.
  • MCP defines the tool surface that n8n is allowed to use.

The agent does not get “access to Laravel.” It gets a small set of well-defined tools.

That distinction matters.

A tool says:

You may start this specific business workflow with these specific inputs, and Laravel will decide what is allowed.

A raw integration says:

Here is the system. Good luck.

One of those can survive production. The other becomes an incident report.

2. Why MCP is the interesting seam between n8n and Laravel

MCP, the Model Context Protocol, is useful here because it gives AI agents a standard way to discover and call capabilities.

Instead of hardcoding ten different REST endpoints into every workflow, an MCP server can expose a structured tool list:

  • Tool name
  • Description
  • Input schema
  • Expected behavior

An MCP client can then discover those tools and use them in a model-driven workflow.

In this setup, Laravel becomes the MCP server. n8n becomes the MCP client, or at least the workflow environment that calls the MCP server through HTTP or an MCP-compatible node.

A simplified tool list from Laravel might look like this:

{
  "tools": [
    {
      "name": "search_orders",
      "description": "Search orders by customer email or order ID.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "orderId": {
            "type": "string"
          }
        }
      }
    },
    {
      "name": "start_refund_review",
      "description": "Start a refund review workflow for an order.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "orderId": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "idempotencyKey": {
            "type": "string",
            "format": "uuid"
          }
        },
        "required": ["orderId", "reason", "idempotencyKey"]
      }
    }
  ]
}

This is more useful than a generic API because it is explicitly model-friendly. The description tells the model what the tool is for. The schema tells it what arguments are required. The backend still validates and enforces everything.

MCP is not magic. It does not make unsafe operations safe. It does not replace authorization. It does not know that refunding a six-month-old enterprise invoice should require human approval.

What it does well is standardize the seam.

That becomes important when you have more than one agent or more than one workflow. Maybe you later add:

  • A support agent in n8n
  • An internal ops agent
  • A Laravel console assistant
  • A Slack-based workflow
  • A scheduled cleanup agent

If all of them use the same Laravel MCP server, you have one place to define and audit the capabilities your AI systems can invoke.

🧠 The important part: MCP is valuable when you want a reusable, discoverable tool layer. If you only need one webhook, MCP may be overkill.

3. Keep Laravel in charge of business truth

A common mistake is making n8n the place where business logic accidentally lives.

It happens slowly.

First, n8n checks a database row. Then it updates a status. Then it decides whether a refund is allowed. Then it calculates pricing. Then it sends an email based on a business rule that nobody documented.

Six months later, the workflow is the business logic, and nobody wants to touch it.

For an AI agent system, this is especially dangerous. n8n is excellent at orchestration, but your Laravel application should usually remain the authority for:

  • Authentication context
  • Authorization rules
  • Domain validation
  • Workflow state
  • Business rules
  • Financial calculations
  • Audit logs
  • Queue dispatching
  • Transactional consistency

Think of n8n as the operator, not the owner.

A good division of responsibility looks like this:

ConcernLaraveln8n
User and tenant data✅❌
Business rules✅❌
Permission checks✅❌
Workflow records✅❌
Model orchestration❌✅
Prompt construction❌✅
External notifications⚠️ optional✅
Retry logic for integrations⚠️ optional✅
MCP tool definitions✅❌
Tool invocation❌✅

This does not mean n8n cannot do meaningful work. It can call external APIs, send Slack messages, wait for approvals, schedule follow-ups, and coordinate multiple tools.

But when the agent says, “I started a refund review,” the actual workflow should be created inside Laravel, validated by Laravel, and tracked by Laravel.

That gives you a single place to answer questions like:

  • Was this workflow allowed?
  • Who initiated it?
  • What input was used?
  • Was it duplicated?
  • Is it still running?
  • Did it finish?
  • What happened next?

If those answers live in five different n8n nodes and two database tables, you have not built an agent system. You have built a mystery.

4. Build a Laravel MCP endpoint that exposes actions, not tables

The most dangerous MCP server is the one that exposes generic database operations.

Avoid tools like:

  • run_sql
  • update_table
  • delete_record
  • query_any_model
  • execute_php

These are not business tools. They are accidents waiting for a creative model.

A production-friendly Laravel MCP endpoint should expose bounded, business-meaningful actions.

For example:

  • search_orders
  • get_subscription_status
  • start_refund_review
  • pause_subscription
  • retry_failed_invoice
  • create_support_ticket
  • get_workflow_run

Each tool should correspond to something your business actually allows.

Here is a practical Laravel implementation pattern.

First, protect the MCP route:

// routes/api.php

Route::post('/mcp', McpController::class)
    ->middleware([
        'auth:sanctum',
        'ability:mcp:invoke',
        'throttle:60,1',
    ]);

Then create a controller that handles a small JSON-RPC surface:

// app/Http/Controllers/McpController.php

namespace App\Http\Controllers;

use App\Support\Mcp\McpToolRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;

class McpController extends Controller
{
    public function __invoke(Request $request, McpToolRegistry $registry): JsonResponse
    {
        $payload = $request->json()->all();

        $id = $payload['id'] ?? null;
        $method = $payload['method'] ?? '';
        $params = $payload['params'] ?? [];

        try {
            $result = match ($method) {
                'tools/list' => [
                    'tools' => $registry->definitions(),
                ],
                'tools/call' => $registry->call(
                    name: $params['name'] ?? '',
                    arguments: $params['arguments'] ?? [],
                    actor: $request->user(),
                ),
                default => throw new \RuntimeException("Method not found: {$method}"),
            };

            return response()->json([
                'jsonrpc' => '2.0',
                'id' => $id,
                'result' => $result,
            ]);
        } catch (ValidationException $e) {
            return response()->json([
                'jsonrpc' => '2.0',
                'id' => $id,
                'error' => [
                    'code' => -32602,
                    'message' => 'Invalid params',
                    'data' => $e->errors(),
                ],
            ], 200);
        } catch (Throwable $e) {
            return response()->json([
                'jsonrpc' => '2.0',
                'id' => $id,
                'error' => [
                    'code' => -32000,
                    'message' => $e->getMessage(),
                ],
            ], 200);
        }
    }
}

This is intentionally minimal. Depending on the MCP client you use, there may also be handshake methods such as initialize. If you are using a dedicated MCP SDK, that part is usually handled for you. If you are wiring n8n through raw HTTP nodes, you can often control exactly which methods you need.

The more important part is the tool registry.

// app/Support/Mcp/McpToolRegistry.php

namespace App\Support\Mcp;

use App\Actions\Mcp\GetWorkflowRun;
use App\Actions\Mcp\SearchOrders;
use App\Actions\Mcp\StartRefundReview;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class McpToolRegistry
{
    public function definitions(): array
    {
        return [
            [
                'name' => 'search_orders',
                'description' => 'Search orders by customer email or order ID.',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'email' => ['type' => 'string', 'format' => 'email'],
                        'orderId' => ['type' => 'string'],
                    ],
                ],
            ],
            [
                'name' => 'start_refund_review',
                'description' => 'Start a refund review workflow for an order.',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'orderId' => ['type' => 'string'],
                        'reason' => ['type' => 'string', 'maxLength' => 500],
                        'idempotencyKey' => ['type' => 'string', 'format' => 'uuid'],
                    ],
                    'required' => ['orderId', 'reason', 'idempotencyKey'],
                ],
            ],
            [
                'name' => 'get_workflow_run',
                'description' => 'Get the current status of a workflow run.',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'workflowRunId' => ['type' => 'string'],
                    ],
                    'required' => ['workflowRunId'],
                ],
            ],
        ];
    }

    public function call(string $name, array $arguments, User $actor): array
    {
        $result = match ($name) {
            'search_orders' => app(SearchOrders::class)->handle($arguments, $actor),
            'start_refund_review' => app(StartRefundReview::class)->handle($arguments, $actor),
            'get_workflow_run' => app(GetWorkflowRun::class)->handle($arguments, $actor),
            default => throw new ValidationException(
                Validator::make([], []),
                ["tool" => ["Unknown tool: {$name}"]]
            ),
        };

        return [
            'content' => [
                [
                    'type' => 'text',
                    'text' => json_encode($result),
                ],
            ],
            'isError' => false,
        ];
    }
}

The important architectural decision is not the JSON-RPC shape. It is that each tool maps to a specific action class.

That keeps the MCP layer thin. The real logic lives in Laravel action classes, services, jobs, and policies.

⚠️ Gotcha: do not return full Eloquent models to the model. Return compact, intentional data. The model does not need created_at, updated_at, internal flags, and every relation.

A good tool result is small enough to reason about:

{
  "orderId": "order_8123",
  "status": "paid",
  "customerEmail": "maya@example.com",
  "total": 129.00,
  "currency": "USD"
}

A bad tool result is a 400-field dump that accidentally includes internal billing metadata.

5. Give n8n a service identity with boring, narrow permissions

The token you give n8n is the real permission boundary.

If that token belongs to an admin user, your AI agent has admin powers. That is usually not what you want.

Instead, create a dedicated service user or API identity for n8n. Give it only the abilities required for the workflows you are building.

With Laravel Sanctum, that can be as simple as creating a token with a narrow ability:

$user = User::where('email', 'n8n-agent@example.com')->firstOrFail();

$token = $user->createToken('n8n-business-agent', ['mcp:invoke']);

// Store this secret in n8n credentials, not in code.
$plainTextToken = $token->plainTextToken;

Then n8n calls the Laravel MCP endpoint with:

Authorization: Bearer YOUR_N8N_TOKEN
Content-Type: application/json

If you use an n8n MCP node or HTTP Request node, put the token in n8n credentials. Do not paste it into workflow expressions, commit it to source control, or expose it to the browser.

But authentication alone is not enough. The service user still needs authorization rules inside Laravel.

For example, the n8n agent may be allowed to read order data, but only for customers in the same tenant. It may be allowed to start refund reviews, but not approve them. It may be allowed to read workflow status, but not cancel workflows.

Those rules should live in Laravel policies or gates.

Example:

// app/Policies/OrderPolicy.php

namespace App\Policies;

use App\Models\Order;
use App\Models\User;

class OrderPolicy
{
    public function view(User $user, Order $order): bool
    {
        return $user->can('mcp:invoke')
            && $user->tenant_id === $order->tenant_id;
    }

    public function startRefundReview(User $user, Order $order): bool
    {
        return $user->can('mcp:invoke')
            && $user->tenant_id === $order->tenant_id
            && in_array($order->status, ['paid', 'partially_refunded'], true);
    }
}

Then use the policy inside the action class:

Gate::forUser($actor)->authorize('startRefundReview', $order);

This gives you a normal Laravel authorization layer instead of a weird AI-specific one.

Why this works:
The agent is not trusted because the model is smart. The agent is limited because the service identity is limited.

That is a much safer place to be.

🚨 Production warning: if your n8n instance is compromised, the blast radius should be limited by the token and Laravel policies. It should not become a master key to your application.

Also rate-limit the MCP endpoint. Agent loops can retry, and model-driven workflows can produce bursts of tool calls. A simple Laravel throttle is a good start:

'throttle:60,1'

For sensitive tools, you may want stricter limits per tool or per user.

6. Design tools around business workflows, not CRUD

The fastest way to create a dangerous agent is to expose generic CRUD.

The fastest way to create a useful agent is to expose business workflows.

A CRUD tool says:

Update this order row.

A workflow tool says:

Start a refund review for this order.

The second one is safer because it has intent.

Intent gives you a place to attach:

  • Validation
  • Authorization
  • Idempotency
  • Audit logging
  • Business rules
  • Queueing
  • Human approval
  • Notifications

For example, instead of exposing update_subscription_status, expose:

pause_subscription
resume_subscription
cancel_subscription_at_period_end

Instead of exposing insert_ticket, expose:

create_support_ticket
escalate_support_ticket

Instead of exposing update_invoice, expose:

retry_failed_invoice
mark_invoice_disputed

The tool name should describe the business operation.

Here is a realistic action class for starting a refund review:

// app/Actions/Mcp/StartRefundReview.php

namespace App\Actions\Mcp;

use App\Jobs\RunRefundReviewWorkflow;
use App\Models\Order;
use App\Models\User;
use App\Models\WorkflowRun;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;

class StartRefundReview
{
    public function handle(array $input, User $actor): array
    {
        $data = Validator::validate($input, [
            'orderId' => ['required', 'string', 'exists:orders,id'],
            'reason' => ['required', 'string', 'max:500'],
            'idempotencyKey' => ['required', 'uuid'],
        ]);

        $order = Order::findOrFail($data['orderId']);

        Gate::forUser($actor)->authorize('startRefundReview', $order);

        $workflow = WorkflowRun::firstOrCreate(
            [
                'idempotency_key' => $data['idempotencyKey'],
            ],
            [
                'type' => 'refund_review',
                'order_id' => $order->id,
                'tenant_id' => $order->tenant_id,
                'status' => 'queued',
                'initiated_by' => $actor->id,
                'input' => [
                    'reason' => $data['reason'],
                ],
            ]
        );

        if ($workflow->wasRecentlyCreated) {
            RunRefundReviewWorkflow::dispatch($workflow);
        }

        return [
            'workflowRunId' => $workflow->id,
            'status' => $workflow->status,
            'orderId' => $order->id,
        ];
    }
}

This pattern does several useful things.

First, it validates the input before doing anything.

Second, it checks authorization through Laravel’s normal policy system.

Third, it creates a WorkflowRun record. That record becomes the receipt for the business operation.

Fourth, it dispatches a queued job instead of doing all the work inside the HTTP request.

Fifth, it returns a small payload that the agent can use to check status later.

That is a much better contract than “here is the database, do your best.”

💡 Practical note: if a tool can change state, give it an idempotency key. If it only reads data, you can usually keep it simpler.

In n8n, the AI agent can choose the tool based on the user’s request, but the tool itself decides what is allowed.

That is the right balance.

7. Long-running workflows need receipts, not blocking responses

Real business workflows are rarely instant.

A refund review may need to:

  • Check payment provider state
  • Notify finance
  • Wait for human approval
  • Send an email
  • Update the order
  • Create a credit note
  • Sync to an accounting system

If your MCP tool tries to do all of that inside one synchronous HTTP call, you will create timeouts, retries, duplicate executions, and sad developers.

The better pattern is:

  1. Tool receives request.
  2. Tool validates and authorizes it.
  3. Tool creates a workflow run.
  4. Tool queues the work.
  5. Tool returns a workflow ID.
  6. Agent can later check the status.

The tool response becomes a receipt:

{
  "workflowRunId": "wfr_01hsz9y3",
  "status": "queued",
  "orderId": "order_8123"
}

Then you expose another tool:

{
  "name": "get_workflow_run",
  "description": "Get the current status of a workflow run.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "workflowRunId": {
        "type": "string"
      }
    },
    "required": ["workflowRunId"]
  }
}

Laravel can implement it like this:

// app/Actions/Mcp/GetWorkflowRun.php

namespace App\Actions\Mcp;

use App\Models\User;
use App\Models\WorkflowRun;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;

class GetWorkflowRun
{
    public function handle(array $input, User $actor): array
    {
        $data = Validator::validate($input, [
            'workflowRunId' => ['required', 'string', 'exists:workflow_runs,id'],
        ]);

        $workflow = WorkflowRun::findOrFail($data['workflowRunId']);

        Gate::forUser($actor)->authorize('view', $workflow);

        return [
            'workflowRunId' => $workflow->id,
            'type' => $workflow->type,
            'status' => $workflow->status,
            'orderId' => $workflow->order_id,
            'createdAt' => $workflow->created_at->toIso8601String(),
            'updatedAt' => $workflow->updated_at->toIso8601String(),
        ];
    }
}

Now the agent can do something honest:

I started the refund review. Its workflow ID is wfr_01hsz9y3. It is currently queued.

That is better than pretending the whole workflow finished instantly.

In n8n, you have a few ways to handle this.

For simple cases, the agent can poll get_workflow_run after a delay.

For better cases, Laravel can call back into n8n when the workflow completes. That callback should be authenticated and idempotent.

Example workflow states:

queued
running
awaiting_approval
completed
failed
cancelled

The important part is that the agent does not become the only place where workflow state exists.

If the n8n workflow crashes, restarts, or times out, Laravel should still know what happened.

This is especially important when the AI agent is user-facing. Users do not care that your orchestration tool retried. They care whether the refund review actually happened.

8. Approvals, idempotency, and audit logs are the real agent features

Once an agent can run business workflows, the hard problems become operational.

The three that matter most are:

  1. Approvals
  2. Idempotency
  3. Audit logs

Approvals

Not every action should happen just because the model asked for it.

Some actions are safe enough to run automatically:

  • Search orders
  • Read subscription status
  • Check workflow state
  • Retrieve documentation

Other actions should require confirmation:

  • Refunds above a threshold
  • Cancelling subscriptions
  • Changing billing emails
  • Sending customer-facing email drafts
  • Exporting sensitive data
  • Modifying access controls

You can model this in Laravel by returning a status like awaiting_approval.

Example:

if ($order->total > 500) {
    $workflow->update([
        'status' => 'awaiting_approval',
    ]);

    return [
        'workflowRunId' => $workflow->id,
        'status' => $workflow->status,
        'message' => 'This refund requires human approval.',
    ];
}

Then the agent can say:

I created the refund request, but it needs approval because the order total is above $500.

That is a much better user experience than the agent silently failing or pretending it completed something.

In n8n, you can build the approval path:

  • Send Slack approval request
  • Wait for webhook callback
  • Update Laravel approval endpoint
  • Continue workflow if approved
  • Notify the agent or customer if rejected

The approval should not be a prompt decision. It should be a system decision.

Idempotency

Agents retry.

Models may ask for the same tool again. n8n may retry after a timeout. A user may click twice. The network may fail after the action succeeds but before the response arrives.

For any state-changing tool, use idempotency keys.

The earlier StartRefundReview example uses this pattern:

$workflow = WorkflowRun::firstOrCreate(
    [
        'idempotency_key' => $data['idempotencyKey'],
    ],
    [
        'type' => 'refund_review',
        'order_id' => $order->id,
        'tenant_id' => $order->tenant_id,
        'status' => 'queued',
        'initiated_by' => $actor->id,
        'input' => [
            'reason' => $data['reason'],
        ],
    ]
);

if ($workflow->wasRecentlyCreated) {
    RunRefundReviewWorkflow::dispatch($workflow);
}

If the same idempotency key arrives again, Laravel returns the existing workflow instead of creating a duplicate.

That single pattern prevents a large category of agent bugs.

🔍 Why this matters: an agent that accidentally creates three refund reviews is not a helpful assistant. It is an operations incident.

Audit logs

Every MCP tool call should leave a trail.

At minimum, log:

  • Tool name
  • Actor or service identity
  • Tenant
  • Input hash or sanitized input
  • Workflow run ID
  • Result status
  • Error message, if any
  • Timestamp
  • Request ID

A simple Laravel listener or observer can do this:

// app/Models/WorkflowRun.php

protected static function booted(): void
{
    static::created(function (WorkflowRun $workflow) {
        AuditLog::create([
            'tenant_id' => $workflow->tenant_id,
            'actor_id' => $workflow->initiated_by,
            'action' => "workflow:{$workflow->type}",
            'subject_type' => WorkflowRun::class,
            'subject_id' => $workflow->id,
            'metadata' => [
                'status' => $workflow->status,
                'input' => $workflow->input,
            ],
        ]);
    });
}

Be careful about what you store in logs. Do not store secrets. Redact sensitive customer data if needed. But do not skip the audit log.

When someone asks, “Why did the agent send that email?” you want an answer.

9. When MCP beats a plain webhook or REST endpoint

MCP is not always the right answer.

Sometimes a webhook is better. Sometimes a normal REST endpoint is better. Sometimes MCP is exactly the right layer.

Here is a practical comparison.

ApproachBest use caseComplexityTool discoveryReusabilityBest when
Plain webhookFire-and-forget eventsLowNoneLowYou only need to trigger one known workflow
REST endpointStandard app integrationLow to mediumNoneMediumYou already have well-defined APIs
MCP serverAI tool layerMedium to highYesHighMultiple agents or workflows need shared capabilities
Direct DB accessAlmost neverLowNoneLowYou enjoy debugging incidents

A webhook is often enough if you only need this:

When something happens in Laravel, call n8n.

For example:

  • Order created
  • Invoice failed
  • Ticket updated
  • Subscription cancelled

That is event-driven automation, not necessarily agent behavior.

A REST endpoint is enough if your AI agent only needs a few fixed operations and you do not care about standardized tool discovery.

MCP becomes more attractive when you want:

  • Dynamic tool discovery
  • Shared tool definitions across agents
  • Model-friendly schemas
  • A reusable capability layer
  • Cleaner separation between orchestration and business rules
  • A path toward multiple AI clients using the same tools

The mistake is using MCP because it is fashionable.

If you have one workflow, one endpoint, and one integration, a simple authenticated HTTP endpoint may be the better engineering decision.

But if you are building a system where AI agents will increasingly interact with business operations, MCP gives you a more deliberate boundary.

It forces you to ask:

  • What tools exist?
  • What inputs do they accept?
  • What are they allowed to do?
  • Who can call them?
  • What happens when they fail?
  • How does the agent know the result?

Those are the right questions.

What I would ship

If I were building this for a real product, I would not start by giving the agent broad access to Laravel.

I would start small and make the system boring.

The first version would include:

  • One Laravel MCP endpoint protected by Sanctum
  • One dedicated n8n service token
  • Two or three read-only tools
  • One low-risk action tool
  • One long-running workflow with a WorkflowRun record
  • One polling tool to check workflow status
  • Idempotency keys for every state-changing tool
  • Audit logs for every tool call
  • Human approval for anything financial or irreversible

A good starter tool set might be:

search_orders
get_subscription_status
create_support_ticket
get_workflow_run
start_refund_review

That is enough to build something useful without giving the agent the keys to everything.

I would also keep the division of responsibility strict:

  • Laravel owns business truth.
  • n8n owns orchestration.
  • MCP defines the tool surface.
  • Policies decide what is allowed.
  • Queues handle slow work.
  • Workflow runs provide receipts.
  • Audit logs provide memory.

The most important realization is that connecting n8n to Laravel is not the hard part.

The hard part is deciding what the agent is allowed to do, how it proves intent, and how your system remains understandable when the workflow fails at 2 a.m.

MCP can make that boundary cleaner. But the boundary is what matters.

Post a Comment

Previous Post Next Post