laravel/mcp: Assert tools/list returns tools in declaration order
Contributed by pushpak1300
Part of the mcp v1.0.1 release
Pull request #320 is about as small as a changelog entry gets: it adds one test to tests/Unit/Methods/ListToolsTest.php and touches nothing else. The test pins down a guarantee the Model Context Protocol specification asks of every server — the tools/list method should return tools in a deterministic order, matching the order in which they were declared. Laravel MCP already honours that guarantee, so this pull request is a regression guard rather than a bug fix. If you are upgrading from v0.9.6, there is nothing to migrate, nothing to enable, and no change in the responses your server emits.
What changed
The diff adds a single test, it('lists tools in a deterministic order matching declaration order'). It builds a ServerContext with four tool classes registered in a deliberately non-alphabetical order — ToolWithOutputSchema, SayHiTool, ToolWithoutOutputSchema, SayHiWithMetaTool — and drives the ListTools method through two scenarios.
First, it issues two full-page requests with per_page: 50 and asserts that both responses match the declaration order and each other. Then it walks the same list through pagination: the context sets defaultPaginationLength: 2, so the four tools split across two pages, and the test asserts that the two pages concatenate back to the declaration order. The names were chosen so any alphabetical sorting would fail immediately — in a sortBy('name') world, say-hi-tool would land before tool-with-output-schema.
Why tool order matters
This is not spec pedantry. The specification says servers should return tools in a deterministic order because clients cache the tools/list response and feed the tool definitions into the model's system prompt. LLM providers cache prompts based on the prompt prefix, so if the tool list shuffles between calls, every cached prompt entry is invalidated and the client pays the full token cost and latency again on every request. A stable order is nearly free for a server to honour, which makes it effectively part of the contract even though the spec words it as a recommendation. An ordering bug here would be invisible in a single request yet expensive in production.
How laravel/mcp already guarantees it
As the pull request notes, the runtime already does the right thing. ServerContext resolves tools by mapping and filtering the declared list in place, so declaration order survives. The paginator calls ->values() before slicing, meaning entries filtered out along the way cannot leave gaps or reorder what remains. And because laravel/mcp requires you to declare tools explicitly rather than discovering them by scanning the filesystem — the autodiscovery approach that has bitten other SDKs — there is no filesystem-ordering input to worry about at all.
In an application, the guaranteed order is simply the order on your server class:
class WeatherForecasterServer extends Server
{
protected array $tools = [
CurrentWeatherTool::class, // returned first…
LookupCityTool::class,
ConvertUnitsTool::class, // …returned last
];
}
Every tools/list call returns those three in exactly that order, whether paginated or not. That also means you are free to declare tools in whatever order makes sense for your model — most-used first, grouped by domain — without needing to alphabetise anything.
Inside the new test
The test works at the unit level, constructing ServerContext and the ListTools method class directly rather than bootstrapping a full server. Here is a condensed version:
it('lists tools in a deterministic order matching declaration order', function (): void {
$context = new ServerContext(
// protocol versions, capabilities, implementation, instructions…
maxPaginationLength: 50,
defaultPaginationLength: 2,
tools: [
ToolWithOutputSchema::class,
SayHiTool::class,
ToolWithoutOutputSchema::class,
SayHiWithMetaTool::class,
],
resources: [],
prompts: [],
);
$declarationOrder = [
'tool-with-output-schema',
'say-hi-tool',
'tool-without-output-schema',
'say-hi-with-meta-tool',
];
// $names() issues a list-tools request and returns [names, nextCursor].
[$first] = $names(['per_page' => 50]);
[$second] = $names(['per_page' => 50]);
expect($first)->toEqual($declarationOrder)
->and($second)->toEqual($first);
[$pageOne, $cursor] = $names([]);
[$pageTwo] = $names(['cursor' => $cursor]);
expect([...$pageOne, ...$pageTwo])->toEqual($declarationOrder);
});
Each half of the assertion blocks a plausible future refactor. Someone "improving" resolvePrimitives() with a sortBy or rebuilding the collection through a name-keyed lookup would fail the full-page comparison, while a change to cursor handling that reordered pages would fail the concatenation check.
Upgrade impact from v0.9.6
None. There is no runtime change in the diff, so your tools/list payloads are identical before and after: same order, same fields, same cursor semantics. No API was added or deprecated, and no configuration is required. The only visible effect of this pull request is that the package's test suite will now catch any future reordering before it reaches a release.
Takeaways
- Test-only change. No runtime code, API surface, or configuration was modified.
- Order is declaration order.
tools/listreturns tools as declared, protecting client prompt caches as the spec recommends. - Pagination preserves order. Pages concatenate back to the declared sequence.
- No upgrade action required. v0.9.6 behaviour is unchanged; the new test simply guards against future regressions.