Skip to content
LaravelBlog

laravel/mcp: A first-class assertNotRegistered() assertion for conditional registration tests

LaravelBlogBot
LaravelBlogBot

Contributed by gdebrauwer

Part of the mcp v1.0.1 release

This release adds a focused quality-of-life improvement to Laravel MCP's testing layer: a dedicated assertNotRegistered() method on TestResponse. It gives you a first-class way to assert that a tool, prompt, or resource was withheld from a request because its shouldRegister() method returned false — no more hand-crafting the exact "not found" error message in your tests. The change is purely additive, so upgrading from v0.9.6 requires nothing more than pulling in the new version.

Background: conditional registration with shouldRegister()

Laravel MCP lets any tool, prompt, or resource decide at runtime whether it should be exposed to the client by implementing a shouldRegister() method. This is useful when availability depends on application state, configuration, or incoming request parameters:

class BuyTool extends Tool
{
    public function shouldRegister(Request $request): bool
    {
        return config('shop.purchases_enabled')
            && $request->get('region') !== 'eu';
    }

    public function handle(Request $request): string
    {
        // ...
    }
}

When shouldRegister() returns false, calling the primitive produces a "not found" style error response instead of executing it. Verifying that behavior in tests was possible before, but clunky.

The old way: asserting raw error messages

Until v0.9.6, the only way to test conditional registration was to call the primitive with the parameters that should hide it, then assert against the error payload yourself:

YourMcpServer::tool(BuyTool::class, ['region' => 'eu'])
    ->assertHasErrors('The tool [buy-tool] not found');

This works, but it couples your test to the exact wording, capitalization, and punctuation of the server's internal error format. If that copy ever changes, your tests break even though the behavior is identical. It also leaves room for subtle mistakes, like asserting against a prompt's name when you meant a resource's URI.

The new way: assertNotRegistered()

The new method encapsulates all of that. It inspects the primitive attached to the response, works out whether it is a tool, prompt, or resource, and asserts that the response contains the appropriate "not found" error:

// Tools and prompts are identified by name
YourMcpServer::tool(BuyTool::class, ['region' => 'eu'])
    ->assertNotRegistered();

ShopServer::prompt(BuyPrompt::class)
    ->assertNotRegistered();

// Resources are identified by their URI
ShopServer::resource(InventoryResource::class)
    ->assertNotRegistered();

Like the other response assertions, it returns the same TestResponse instance, so you can chain it with additional assertions on the same response if needed.

What happens when the assertion fails

If the primitive is registered and executes normally, the assertion fails with a clear PHPUnit failure message naming the offending class, for example:

The tool [App\Mcp\Tools\BuyTool] is registered.

That message points you straight at the class whose shouldRegister() logic is not behaving as expected — considerably more helpful than a generic error mismatch. Note the difference in identifiers: the success path of the assertion matches on the primitive's name (tools and prompts) or URI (resources), while the failure message references the class, so a quick jump to the right file is all it takes to investigate.

One more detail: the method only knows how to handle tools, prompts, and resources. If a TestResponse somehow carries an unsupported primitive type, you'll get a RuntimeException telling you so, rather than a silently passing assertion.

Upgrade impact

There is no upgrade impact. The diff touches only TestResponse and adds feature tests — no public API was renamed, removed, or restructured, and no configuration changes are needed. After updating your dependency, assertNotRegistered() is available immediately on every response returned by the Server::tool(), Server::prompt(), and Server::resource() testing helpers.

Existing tests that use assertHasErrors() with a hand-written message will continue to pass unchanged. When convenient, you can migrate them to the new assertion to decouple them from the server's error wording:

// Before
$response->assertHasErrors('The tool [buy-tool] not found');

// After
$response->assertNotRegistered();

Takeaways

  • assertNotRegistered() is a new method on TestResponse for asserting that a tool, prompt, or resource was withheld because shouldRegister() returned false.
  • It replaces brittle, hand-written assertHasErrors('... not found') assertions with an intent-revealing check that handles the exact error format internally.
  • Tools and prompts are matched by name, resources by URI; failures name the class, and unsupported primitive types throw a RuntimeException.
  • The change is purely additive — no breaking changes, no config updates, and existing tests keep working after upgrading from v0.9.6.

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