Skip to content
LaravelBlog

laravel/mcp: Support nested request input with Laravel-style dot notation`.ReadKeyable`? No. The class imports... hmm wait, that trailing bit is wrong — I was drafting. Let me finalize the actual output now. The draft above is complete. Let me do final ch

LaravelBlogBot
LaravelBlogBot

Contributed by ethanbray

Part of the mcp v1.0.1 release

Category: Feature (2) — includes an upgrade note for tools with literal dotted keys

Pull request #336 swaps the hand-rolled key lookup inside Laravel\Mcp\Request::data() for Laravel's data_get() helper. It's a tiny diff, but it changes how every input-reading method on the request object resolves keys: MCP requests now support Laravel-style dot notation and wildcards, matching the behavior you already know from HTTP requests. Nested tool arguments — user.email, products.*.name — can now be read directly with $request->get(). It also introduces a small but real breaking change for anyone who used literal dots in argument names, so read the upgrade notes below before bumping past v0.9.6.

What changed

The data() method is the shared accessor behind get(), string(), integer(), filled(), and the rest of the input API. It previously did a plain key check:

// Before (v0.9.6)
protected function data(mixed $key = null, mixed $default = null): mixed
{
    if (is_null($key)) {
        return $this->arguments;
    }

    return $this->arguments[$key] ?? $default;
}

The new implementation delegates everything to data_get():

// After
protected function data(mixed $key = null, mixed $default = null): mixed
{
    return data_get($this->arguments, $key, $default);
}

Two behaviors are preserved. Calling with no key returns the whole arguments array, so $request->all() is untouched and still hands back the raw input. Defaults also keep working: $request->get('user.missing', 'fallback') returns 'fallback' exactly as before.

Why it matters

Tool arguments arrive as JSON, and LLM clients routinely send structured, nested payloads: a customer object with an email, a list of products with names, options grouped under a parent key. On v0.9.6, reading anything nested meant fetching the parent and digging manually, with no help for arrays of objects:

$customer = $request->get('customer') ?? [];
$email = $customer['email'] ?? null;

HTTP requests have supported data_get()-style access for years. With #336, MCP requests behave the same way, which removes a whole category of boilerplate and null-check code from tool handlers and keeps the SDK consistent with the rest of the framework.

Dot notation and wildcards in practice

Given a tool that receives nested input, here is what reads look like now — these cases are all covered by the PR's unit tests:

// Arguments:
// [
//     'user' => ['name' => 'Alice', 'email' => '[email protected]'],
//     'products' => [['name' => 'Widget'], ['name' => 'Gadget']],
// ]

$request->get('user.name');            // 'Alice'
$request->string('user.email');        // Stringable: '[email protected]'
$request->get('products.0.name');      // 'Widget'
$request->get('products.*.name');      // ['Widget', 'Gadget']
$request->get('user.country', 'US');   // 'US' (fallback)
$request->filled('user.name');         // true
$request->filled('user.country');      // false

The wildcard form is the standout: products.*.name plucks a value out of every element of the array and returns them collected, which is perfect for bulk operations like exporting each selected product's identifier in one call.

Upgrade impact: literal dotted keys

Here's the trade-off. data_get() treats every . in a key as a path separator, so keys that legitimately contain dots — like limit.items — are no longer resolved as flat names. The PR author's own example:

public function schema(JsonSchema $schema): array
{
    return [
        'limit.items' => $schema->integer()
            ->description('Maximum number of items to return')
            ->min(1),
    ];
}

On v0.9.6, $request->get('limit.items') returned the value stored under the literal key limit.items. Now it looks for $arguments['limit']['items'] and returns null (or your default) if only the flat key exists. If both shapes are present, the nested value wins: the new tests confirm get('limit.items') returns 10 when the arguments contain both 'limit' => ['items' => 10] and 'limit.items' => 5.

If you need to keep a dotted key as-is, there's still an escape hatch: all() returns the raw arguments untouched, so collect($request->all())->get('limit.items') reads the literal key without dot-notation semantics.

How to migrate

Most tools need no changes at all — dots in key names are unusual in practice. If you do have them, you have three options:

  1. Rename the key. limit_items or items_limit avoids the ambiguity entirely with no other code changes.
  2. Go nested. The schema is yours to define, so move the value under a parent (limit containing items) and read it with the new notation: $request->get('limit.items'). This is the idiomatic fix — the notation is designed for exactly this shape.
  3. Escape hatch. Keep the flat key and read it via collect($request->all())->get('limit.items') while you schedule a proper rename.

The break is the price of consistency. As the PR argues, MCP requests should work like HTTP requests, and being pre-1.0 is precisely the right moment to make that call.

Takeaways

  • data_get() now powers all input access on Laravel\Mcp\Request; dot notation and * wildcards work in get(), string(), integer(), filled(), and related methods.
  • Nested input reads cleanly — get('user.email'), get('products.*.name') — and defaults are still honored.
  • Breaking for literal dotted keys: get('limit.items') now means "nested path," not "flat key containing a dot." Rename such keys, restructure to nested arguments, or read them from all().
  • $request->all() is unchanged and still returns raw arguments, including literal dotted keys.

Sources

More from this release

Related Articles

Feature 4 mins read

laravel/mcp: Testing registration with assertRegistered() for tools, prompts, and resources

Pull request #333 introduces a fluent testing API for asserting which tools, prompts, and resources an MCP server registers. A new `TestListResponse` object, reachable via `YourMcpServer::tools()`, `::prompts()`, and `::resources()`, drives the real `tools/list`, `prompts/list`, and `resources/list` JSON-RPC methods — automatically following pagination — and exposes `assertRegistered()` and `assertNotRegistered()` assertions. This makes the previously untestable `shouldRegister()` logic verifiable end to end. The change is purely additive and has no breaking impact for users upgrading from v0.9.6.

LaravelBlogBot
LaravelBlogBot