PHPackages                             sandermuller/laravel-x402-mcp - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. sandermuller/laravel-x402-mcp

ActiveLibrary[Payment Processing](/categories/payments)

sandermuller/laravel-x402-mcp
=============================

Bridge between laravel/mcp and laravel-x402 — gate MCP tools behind x402 stablecoin payments.

0.3.0(1mo ago)10[1 PRs](https://github.com/SanderMuller/laravel-x402-mcp/pulls)MITPHPPHP ^8.2CI passing

Since May 9Pushed 3w agoCompare

[ Source](https://github.com/SanderMuller/laravel-x402-mcp)[ Packagist](https://packagist.org/packages/sandermuller/laravel-x402-mcp)[ Docs](https://github.com/sandermuller/laravel-x402-mcp)[ RSS](/packages/sandermuller-laravel-x402-mcp/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (26)Versions (6)Used By (0)

laravel-x402-mcp
================

[](#laravel-x402-mcp)

[![Latest Version on Packagist](https://camo.githubusercontent.com/96da59eb23ed30e5dce7fb2d93eec5130cdd34cd89fcf740383636cd8c386ea8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73616e6465726d756c6c65722f6c61726176656c2d783430322d6d63702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/sandermuller/laravel-x402-mcp)[![GitHub Tests Action Status](https://camo.githubusercontent.com/46cea9367d216bbf43f17f3d655c74b57fbb5683711aae76230aaed5a7c457a5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f73616e6465726d756c6c65722f6c61726176656c2d783430322d6d63702f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/sandermuller/laravel-x402-mcp/actions/workflows/run-tests.yml)[![Total Downloads](https://camo.githubusercontent.com/a5a2ce303d5d0aaf4e50d2983406050c552841e027fb9a448b9c3787438d86cf/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73616e6465726d756c6c65722f6c61726176656c2d783430322d6d63702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/sandermuller/laravel-x402-mcp)[![License](https://camo.githubusercontent.com/f2b6262167e61d19c0effe7a4f2b4a334d0c9f0389d901a2d2ccbcae3e605aa2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f73616e6465726d756c6c65722f6c61726176656c2d783430322d6d63702e7376673f7374796c653d666c61742d737175617265)](LICENSE)

Gate [`laravel/mcp`](https://github.com/laravel/mcp) tools behind x402 stablecoin payments. Conformant with the x402 v2 MCP transport spec (`specs/transports-v2/mcp.md`).

Bridge between [`sandermuller/laravel-x402`](https://github.com/sandermuller/laravel-x402) (^0.5) and `laravel/mcp` (^0.6 || ^0.7). Annotate paid tools with the `#[X402Price]` attribute. Agents include the signed payment payload in `params._meta["x402/payment"]` (JSON-RPC level — not an HTTP header). The advertised price travels back on `tools/list` / `resources/list` / `prompts/list` via `_meta["x402/price"]`.

Install
-------

[](#install)

```
composer require sandermuller/laravel-x402-mcp
```

The bridge inherits its facilitator wiring, recipient address, and asset config from `laravel-x402`. Run that package's installer first:

```
php artisan x402:install
```

This sets `X402_RECIPIENT` and (optionally) `X402_PRIVATE_KEY` in `.env` and publishes the `config/x402.php` file. Verify with `php artisan x402:verify-config`.

Usage
-----

[](#usage)

### 1. Annotate a paid tool

[](#1-annotate-a-paid-tool)

```
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
use X402\Laravel\Mcp\Attributes\X402Price;

#[X402Price(amount: '0.01', asset: 'USDC', network: 'base')]
final class FetchPremiumData extends Tool
{
    public function description(): string
    {
        return 'Premium dataset. Costs $0.01 USDC on Base.';
    }

    public function handle(Request $request): Response
    {
        // Runs only after payment is settled.
        return Response::json(['data' => '...']);
    }
}
```

`payTo` overrides the global `x402.recipient` for a specific tool:

```
#[X402Price(amount: '5.00', asset: 'USDC', network: 'base', payTo: '0xa11ce...')]
```

### 2. Wire the gating method handlers on your Server

[](#2-wire-the-gating-method-handlers-on-your-server)

```
use Laravel\Mcp\Server\Server;
use X402\Laravel\Mcp\Server\Concerns\WithX402Payment;

final class MyMcpServer extends Server
{
    use WithX402Payment;

    protected array $tools = [
        FetchPremiumData::class,
    ];
}
```

The `WithX402Payment` trait registers six method handlers when the server starts:

- `tools/list` → `X402ListTools` — advertises priced tools as `_meta["x402/price"]` so agents know the price *before* invoking.
- `tools/call` → `X402CallTool` — gates priced tools behind a verified + settled payment; passes free tools through unchanged.
- `resources/list` → `X402ListResources` — advertises priced resources as `_meta["x402/price"]` (templates excluded; they list under `resources/templates/list`).
- `resources/read` → `X402ReadResource` — gates priced `Resource` subclasses. The resource's URI is the challenge resource verbatim (no synthetic prefix).
- `prompts/list` → `X402ListPrompts` — advertises priced prompts as `_meta["x402/price"]`.
- `prompts/get` → `X402GetPrompt` — gates priced `Prompt` subclasses. Synthesises `mcp://prompt/{name}` for the challenge resource.

The trait hooks on `start()`, not `boot()`, so any subclass overriding `boot()` still gets x402 gating without having to know about this trait. If you want to opt *out* of trait defaults — for example to register your own `tools/call` handler — use `addMethod()` inside `boot()`; explicit registrations made there win over the trait. If you also override `start()`, call `parent::start()` so the trait runs.

### Gating resources and prompts

[](#gating-resources-and-prompts)

The same `#[X402Price]` attribute applies to `Resource` and `Prompt` subclasses:

```
use Laravel\Mcp\Server\Resource;
use X402\Laravel\Mcp\Attributes\X402Price;

#[X402Price(amount: '0.05', asset: 'USDC', network: 'base')]
final class PremiumDataset extends Resource
{
    protected string $uri = 'data://premium/v1';

    public function handle(): Response
    {
        return Response::text('...');
    }
}
```

Differences from tool gating:

- **Challenge URI shape.** Tools use `mcp://tool/{name}`. Resources use the request URI verbatim (e.g. `data://premium/v1`) — they're already URI-addressed. Prompts use `mcp://prompt/{name}`.
- **`HasUriTemplate` resources.** A priced template gates *every* concrete URI under it — `parent::invokeResource` is used (not a bare container call), so URI-template variables stay bound to the request inside the resource handler.
- **Error envelope shape.** A 402 challenge for a paid resource or prompt uses the same `result.isError + structuredContent: PaymentRequired + content[0].text` envelope as `tools/call`. `X402ReadResource` and `X402GetPrompt` both implement `Errable` so the envelope serialises as a JSON-RPC `result`, not a JSON-RPC error.

### 3. What `X402CallTool` does

[](#3-what-x402calltool-does)

1. Looks up the invoked tool, checks for `#[X402Price]`.
2. If unpriced — passes through to the standard `CallTool`.
3. If priced — reads `params._meta["x402/payment"]` for the signed payload, verifies + settles via the bound `FacilitatorClient`, then runs the tool.
4. On success, injects `result._meta["x402/payment-response"]` with the settlement receipt.
5. On any failure, returns a tool result with `isError: true` + `structuredContent: PaymentRequired` + `content[0].text` (JSON-stringified).

The replay store from `laravel-x402` is reused — concurrent requests with the same authorization are rejected before hitting the facilitator.

### Post-settle tool failure

[](#post-settle-tool-failure)

Settlement happens *before* the tool runs. If the tool throws after the facilitator has settled, the payment has already moved on-chain and is not refundable from this layer. **The settlement receipt always lands on the response** — every `Throwable` thrown by the tool (synchronous or mid-stream in a generator) is caught, returned as a tool error result, and stamped with `result._meta["x402/payment-response"]` so agents can prove the payment settled even when delivery failed. Two exceptions to that guarantee:

- `JsonRpcException` — a tool that throws this is signalling an explicit transport-level protocol error; it surfaces as a JSON-RPC error envelope, not a tool result, and carries no receipt.
- The agent disconnects mid-stream — the receipt was generated but never reached the wire. The on-chain settlement stands; a future idempotency cache will let retries replay the cached response.

This ordering is by design: the x402 settle is the canonical proof of payment, and the spec requires it to be observable independently of tool execution. If your tool needs transactional "execute-or-refund" semantics, do the work in two steps — settle the user into a credit balance first, debit on successful execution — rather than relying on this layer.

Wire format
-----------

[](#wire-format)

Per [`specs/transports-v2/mcp.md`](https://github.com/coinbase/x402/blob/main/specs/transports-v2/mcp.md):

DirectionLocationShapeClient → Server (payment)`params._meta["x402/payment"]``PaymentPayload` v2 envelopeServer → Client (settled)`result._meta["x402/payment-response"]``{success, transaction, network, payer}`Server → Client (required)`result.structuredContent` + `result.content[0].text` + `result.isError = true``PaymentRequired`Server → Client (advertised)`_meta["x402/price"]` on each item of `tools/list` / `resources/list` / `prompts/list``{amount, asset, network[, payTo]}`The same `_meta["x402/payment"]` / `_meta["x402/payment-response"]` envelope and the same 402 challenge shape apply to `resources/read` and `prompts/get` — the gating mirrors `tools/call` 1:1, only the challenge resource URI differs (resources use the request URI verbatim; prompts use `mcp://prompt/{name}`).

The HTTP-level `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE` headers used by the x402 HTTP transport are **NOT** used in MCP — payment travels at the JSON-RPC layer, inside the request/response body.

Idempotency
-----------

[](#idempotency)

A transport drop between facilitator-settle and JSON-RPC delivery would otherwise leave the user paid without recourse: the agent retries the same signed authorization, the replay-guard rejects the duplicate nonce with `replay_attempt`, and there's no path back. The bundled `PaidToolResponseCache` closes that gap — the same idea as `laravel-x402` 0.3's `x402.cache` middleware, applied to JSON-RPC `tools/call`, `resources/read`, and `prompts/get`.

How it works:

1. Before claiming the nonce, the handler computes a `CacheScope` (`tools/call|mcp://tool/{name}|sha256(canonical_args)` etc.) and looks up a cached response keyed by `(scope, signature)`.
2. On HIT: rebuild the cached `JsonRpcResponse` with the new request's `id` and return it. No facilitator round-trip, no nonce burn.
3. On MISS: claim the nonce, settle, run the primitive, store the result under `(scope, signature)`, return.

What's pinned in the key:

- **Scope segments** — method (`tools/call` / `resources/read` / `prompts/get`), challenge resource URI (`mcp://tool/{name}` / concrete resource URI / `mcp://prompt/{name}`), and a `sha256` of the canonical-JSON `params.arguments` for tools and prompts. Two reads of `mcp://x/users/1` and `mcp://x/users/2` under the same priced template never collide; same payload + different tool name falls through to `guardReplay` and gets `replay_attempt`.
- **Forge-resistance binding** — the EIP-3009 `signature` field. An attacker who observed the public `(network, from, nonce)` tuple post-settlement cannot produce a different request that hashes to the same key without the private key.

What's NOT cached:

- **402 challenges** — every retry must reprompt for payment.
- **Streaming responses** (`Generator` returns) — there's no atomic snapshot to replay.
- **Tool / resource / prompt errors** (`isError: true`) — a primitive that errored on a settled payment may have transient state; retries deserve a fresh handler call.
- **Sub-second concurrent retries** — the lookup-then-claim ordering does not eliminate retry storms. Two retries within milliseconds can both miss the lookup; one wins `guardReplay`, the other gets `replay_attempt`. Real MCP transports rarely fire sub-second retries (HTTP/2 retry policies and stdio reuse a single in-flight call), so the gap is mostly theoretical. Revisit with a pending-reservation pattern if a production deployment surfaces a real complaint.

Configuration. The cache-store name and TTL are shared with `laravel-x402`'s HTTP middleware (one knob across both transports); the cache prefix is MCP-namespaced so HTTP and JSON-RPC consumers can co-exist on a shared Redis without colliding.

KeyDefaultEffect`x402.response_cache.cache_store``null` (Laravel default store)PSR-16-bridged store name`x402.response_cache.ttl``3600`Idempotency window in seconds`x402_mcp.response_cache.prefix``x402:idem:mcp:`JSON-RPC cache-key prefixThe store binding is via `laravel-x402`'s `LaravelPsr16Bridge` over Illuminate's cache repository — bind any PSR-16 backend (Redis, file, array). The TTL must comfortably exceed the nonce-store's TTL so a retry that arrives after the nonce expires still hits the response cache. Use a persistent store (Redis) in production; the array driver is process-local and only useful in tests.

Stdio transport
---------------

[](#stdio-transport)

Stdio MCP servers can also receive `_meta["x402/payment"]` because `_meta` is a JSON-RPC field, not an HTTP envelope. Paid tools work on stdio as well as HTTP.

Operator commands
-----------------

[](#operator-commands)

```
php artisan x402-mcp:list-tools "App\\Mcp\\Servers\\MyMcpServer"
```

Lists every tool, resource, and prompt on the given Server class with a `Type` column, marking gated entries with their amount, asset, network, and `payTo` (or `(default)` when not overridden). Free entries render as `(free)`. Mirrors `x402:list-routes` from `laravel-x402` for the JSON-RPC transport.

Testing
-------

[](#testing)

`laravel-x402` ships a recording fake. Swap it in once at the top of a test and the bridge picks it up automatically:

```
use X402\Laravel\Facades\X402;

$fake = X402::fake();

// Drive the MCP server however you normally would (HTTP, stdio, in-process).
$this->postJson('/mcp', $jsonRpcCall)->assertOk();

$fake->assertVerified('mcp://tool/fetch-premium-data');
$fake->assertSettled('mcp://tool/fetch-premium-data');
```

`PaymentSettled` / `PaymentRejected` events still fire through `DispatchingFacilitator`, so `Event::fake([PaymentSettled::class])` composes alongside.

License
-------

[](#license)

MIT.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance95

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

3

Last Release

30d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/abeb7bd51fd77656b4133bdcdf4eca99cc8b495b83ca72f63674ff15f2b72a39?d=identicon)[SanderMuller](/maintainers/SanderMuller)

---

Top Contributors

[![SanderMuller](https://avatars.githubusercontent.com/u/9074391?v=4)](https://github.com/SanderMuller "SanderMuller (17 commits)")

---

Tags

laravelmcpModel Context Protocolai-agentsx402paid-tools

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/sandermuller-laravel-x402-mcp/health.svg)

```
[![Health](https://phpackages.com/badges/sandermuller-laravel-x402-mcp/health.svg)](https://phpackages.com/packages/sandermuller-laravel-x402-mcp)
```

###  Alternatives

[laravel/boost

Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.

3.5k17.6M507](/packages/laravel-boost)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76318.2M110](/packages/laravel-mcp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
