Skip to content

Commit 1d3fbfe

Browse files
authored
Merge pull request #182 from laravel/fix/wildcard_issue_in_routes
Clarify ListRoutes name parameter description for better tool calling
2 parents c3b5a4f + 2fb7de1 commit 1d3fbfe

File tree

3 files changed

+163
-15
lines changed

3 files changed

+163
-15
lines changed

src/Mcp/Tools/ListRoutes.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ public function description(): string
2323
public function schema(ToolInputSchema $schema): ToolInputSchema
2424
{
2525
// Mirror the most common `route:list` options. All are optional.
26-
$schema->string('method')->description('Filter the routes by HTTP method.')->required(false);
27-
$schema->string('action')->description('Filter the routes by action.')->required(false);
28-
$schema->string('name')->description('Filter the routes by name.')->required(false);
26+
$schema->string('method')->description('Filter the routes by HTTP method (e.g., GET, POST, PUT, DELETE).')->required(false);
27+
$schema->string('action')->description('Filter the routes by controller action (e.g., UserController@index, ChatController, show).')->required(false);
28+
$schema->string('name')->description('Filter the routes by route name (no wildcards supported).')->required(false);
2929
$schema->string('domain')->description('Filter the routes by domain.')->required(false);
3030
$schema->string('path')->description('Only show routes matching the given path pattern.')->required(false);
3131
// Keys with hyphens are converted to underscores for PHP variable compatibility.
@@ -58,8 +58,11 @@ public function handle(array $arguments): ToolResult
5858
];
5959

6060
foreach ($optionMap as $argKey => $cliOption) {
61-
if (array_key_exists($argKey, $arguments) && ! empty($arguments[$argKey]) && $arguments[$argKey] !== '*') {
62-
$options['--'.$cliOption] = $arguments[$argKey];
61+
if (! empty($arguments[$argKey])) {
62+
$sanitizedValue = str_replace(['*', '?'], '', $arguments[$argKey]);
63+
if (filled($sanitizedValue)) {
64+
$options['--'.$cliOption] = $sanitizedValue;
65+
}
6366
}
6467
}
6568

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Support\Facades\Route;
6+
use Laravel\Boost\Mcp\Tools\ListRoutes;
7+
8+
beforeEach(function () {
9+
Route::get('/admin/dashboard', function () {
10+
return 'admin dashboard';
11+
})->name('admin.dashboard');
12+
13+
Route::post('/admin/users', function () {
14+
return 'admin users';
15+
})->name('admin.users.store');
16+
17+
Route::get('/user/profile', function () {
18+
return 'user profile';
19+
})->name('user.profile');
20+
21+
Route::get('/api/two-factor/enable', function () {
22+
return 'two-factor enable';
23+
})->name('two-factor.enable');
24+
25+
Route::get('/api/v1/posts', function () {
26+
return 'posts';
27+
})->name('api.posts.index');
28+
29+
Route::put('/api/v1/posts/{id}', function ($id) {
30+
return 'update post';
31+
})->name('api.posts.update');
32+
});
33+
34+
test('it returns list of routes without filters', function () {
35+
$tool = new ListRoutes;
36+
$result = $tool->handle([]);
37+
38+
expect($result)->isToolResult()
39+
->toolHasNoError()
40+
->toolTextContains('GET|HEAD', 'admin.dashboard', 'user.profile');
41+
});
42+
43+
test('it sanitizes name parameter wildcards and filters correctly', function () {
44+
$tool = new ListRoutes;
45+
46+
$result = $tool->handle(['name' => '*admin*']);
47+
48+
expect($result)->isToolResult()
49+
->toolHasNoError()
50+
->toolTextContains('admin.dashboard', 'admin.users.store')
51+
->and($result)->not->toolTextContains('user.profile', 'two-factor.enable');
52+
53+
$result = $tool->handle(['name' => '*two-factor*']);
54+
55+
expect($result)->toolTextContains('two-factor.enable')
56+
->and($result)->not->toolTextContains('admin.dashboard', 'user.profile');
57+
58+
$result = $tool->handle(['name' => '*api*']);
59+
60+
expect($result)->toolTextContains('api.posts.index', 'api.posts.update')
61+
->and($result)->not->toolTextContains('admin.dashboard', 'user.profile');
62+
63+
});
64+
65+
test('it sanitizes method parameter wildcards and filters correctly', function () {
66+
$tool = new ListRoutes;
67+
68+
$result = $tool->handle(['method' => 'GET*POST']);
69+
70+
expect($result)->isToolResult()
71+
->toolHasNoError()
72+
->toolTextContains('ERROR Your application doesn\'t have any routes matching the given criteria.');
73+
74+
$result = $tool->handle(['method' => '*GET*']);
75+
76+
expect($result)->toolTextContains('admin.dashboard', 'user.profile', 'api.posts.index')
77+
->and($result)->not->toolTextContains('admin.users.store');
78+
79+
$result = $tool->handle(['method' => '*POST*']);
80+
81+
expect($result)->toolTextContains('admin.users.store')
82+
->and($result)->not->toolTextContains('admin.dashboard');
83+
});
84+
85+
test('it handles edge cases and empty results correctly', function () {
86+
$tool = new ListRoutes;
87+
88+
$result = $tool->handle(['name' => '*']);
89+
90+
expect($result)->isToolResult()
91+
->toolHasNoError()
92+
->toolTextContains('admin.dashboard', 'user.profile', 'two-factor.enable');
93+
94+
$result = $tool->handle(['name' => '*nonexistent*']);
95+
96+
expect($result)->toolTextContains('ERROR Your application doesn\'t have any routes matching the given criteria.');
97+
98+
$result = $tool->handle(['name' => '']);
99+
100+
expect($result)->toolTextContains('admin.dashboard', 'user.profile');
101+
});
102+
103+
test('it handles multiple parameters with wildcard sanitization', function () {
104+
$tool = new ListRoutes;
105+
106+
$result = $tool->handle([
107+
'name' => '*admin*',
108+
'method' => '*GET*',
109+
]);
110+
111+
expect($result)->isToolResult()
112+
->toolHasNoError()
113+
->toolTextContains('admin.dashboard')
114+
->and($result)->not->toolTextContains('admin.users.store', 'user.profile');
115+
116+
$result = $tool->handle([
117+
'name' => '*user*',
118+
'method' => '*POST*',
119+
]);
120+
121+
expect($result)->toolTextContains('admin.users.store');
122+
});
123+
124+
test('it handles the original problematic wildcard case', function () {
125+
$tool = new ListRoutes;
126+
127+
$result = $tool->handle(['name' => '*two-factor*']);
128+
129+
expect($result)->isToolResult()
130+
->toolHasNoError()
131+
->toolTextContains('two-factor.enable');
132+
});

tests/Pest.php

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,29 @@
1515

1616
uses(Tests\TestCase::class)->in('Feature');
1717

18-
/*
19-
|--------------------------------------------------------------------------
20-
| Expectations
21-
|--------------------------------------------------------------------------
22-
|
23-
| When you're writing tests, you often need to check that values meet certain conditions. The
24-
| "expect()" function gives you access to a set of "expectations" methods that you can use
25-
| to assert different things. Of course, you may extend the Expectation API at any time.
26-
|
27-
*/
18+
expect()->extend('isToolResult', function () {
19+
return $this->toBeInstanceOf(\Laravel\Mcp\Server\Tools\ToolResult::class);
20+
});
21+
22+
expect()->extend('toolTextContains', function (mixed ...$needles) {
23+
/** @var \Laravel\Mcp\Server\Tools\ToolResult $this->value */
24+
$output = implode('', array_column($this->value->toArray()['content'], 'text'));
25+
expect($output)->toContain(...func_get_args());
26+
27+
return $this;
28+
});
29+
30+
expect()->extend('toolHasError', function () {
31+
expect($this->value->toArray()['isError'])->toBeTrue();
32+
33+
return $this;
34+
});
35+
36+
expect()->extend('toolHasNoError', function () {
37+
expect($this->value->toArray()['isError'])->toBeFalse();
38+
39+
return $this;
40+
});
2841

2942
function fixture(string $name): string
3043
{

0 commit comments

Comments
 (0)