diff --git a/config/mcp.php b/config/mcp.php new file mode 100644 index 0000000..5165ce1 --- /dev/null +++ b/config/mcp.php @@ -0,0 +1,23 @@ + [ + '*', + // 'https://example.com', + ], + +]; diff --git a/src/Server/Http/Controllers/OAuthRegisterController.php b/src/Server/Http/Controllers/OAuthRegisterController.php new file mode 100644 index 0000000..5a85c76 --- /dev/null +++ b/src/Server/Http/Controllers/OAuthRegisterController.php @@ -0,0 +1,74 @@ +validate([ + 'redirect_uris' => ['required', 'array', 'min:1'], + 'redirect_uris.*' => ['required', 'url', function (string $attribute, $value, $fail): void { + if (in_array('*', config('mcp.redirect_domains', []), true)) { + return; + } + + if (! Str::startsWith($value, $this->allowedDomains())) { + $fail($attribute.' is not a permitted redirect domain.'); + } + }], + ]); + + $clients = Container::getInstance()->make( + "Laravel\Passport\ClientRepository" + ); + + $client = $clients->createAuthorizationCodeGrantClient( + name: $request->get('name'), + redirectUris: $validated['redirect_uris'], + confidential: false, + user: null, + enableDeviceFlow: false, + ); + + return response()->json([ + 'client_id' => (string) $client->id, + 'grant_types' => $client->grantTypes, + 'response_types' => ['code'], + 'redirect_uris' => $client->redirectUris, + 'scope' => 'mcp:use', + 'token_endpoint_auth_method' => 'none', + ]); + } + + /** + * Get the allowed redirect domains. + * + * @return array + */ + protected function allowedDomains(): array + { + /** @var array */ + $allowedDomains = config('mcp.redirect_domains', []); + + return collect($allowedDomains) + ->map(fn (string $domain): string => Str::endsWith($domain, '/') + ? $domain + : "{$domain}/" + ) + ->all(); + } +} diff --git a/src/Server/McpServiceProvider.php b/src/Server/McpServiceProvider.php index c9bcb1f..e9972e9 100644 --- a/src/Server/McpServiceProvider.php +++ b/src/Server/McpServiceProvider.php @@ -19,6 +19,8 @@ class McpServiceProvider extends ServiceProvider public function register(): void { $this->app->singleton(Registrar::class, fn (): Registrar => new Registrar); + + $this->mergeConfigFrom(__DIR__.'/../../config/mcp.php', 'mcp'); } public function boot(): void @@ -48,6 +50,10 @@ protected function registerPublishing(): void __DIR__.'/../../stubs/server.stub' => base_path('stubs/server.stub'), __DIR__.'/../../stubs/tool.stub' => base_path('stubs/tool.stub'), ], 'mcp-stubs'); + + $this->publishes([ + __DIR__.'/../../config/mcp.php' => config_path('mcp.php'), + ], 'mcp-config'); } protected function registerRoutes(): void diff --git a/src/Server/Registrar.php b/src/Server/Registrar.php index 85d6571..47fe3e7 100644 --- a/src/Server/Registrar.php +++ b/src/Server/Registrar.php @@ -5,12 +5,12 @@ namespace Laravel\Mcp\Server; use Illuminate\Container\Container; -use Illuminate\Http\Request; use Illuminate\Routing\Route; use Illuminate\Support\Facades\Route as Router; use Illuminate\Support\Str; use Laravel\Mcp\Server; use Laravel\Mcp\Server\Contracts\Transport; +use Laravel\Mcp\Server\Http\Controllers\OAuthRegisterController; use Laravel\Mcp\Server\Middleware\AddWwwAuthenticateHeader; use Laravel\Mcp\Server\Middleware\ReorderJsonAccept; use Laravel\Mcp\Server\Transport\HttpTransport; @@ -102,30 +102,7 @@ public function oauthRoutes(string $oauthPrefix = 'oauth'): void 'grant_types_supported' => ['authorization_code', 'refresh_token'], ]))->name('mcp.oauth.authorization-server'); - Router::post($oauthPrefix.'/register', function (Request $request) { - $clients = Container::getInstance()->make( - "Laravel\Passport\ClientRepository" - ); - - $payload = $request->json()->all(); - - $client = $clients->createAuthorizationCodeGrantClient( - name: $payload['client_name'], - redirectUris: $payload['redirect_uris'], - confidential: false, - user: null, - enableDeviceFlow: false, - ); - - return response()->json([ - 'client_id' => (string) $client->id, - 'grant_types' => $client->grantTypes, - 'response_types' => ['code'], - 'redirect_uris' => $client->redirectUris, - 'scope' => 'mcp:use', - 'token_endpoint_auth_method' => 'none', - ]); - }); + Router::post($oauthPrefix.'/register', OAuthRegisterController::class); } /** diff --git a/tests/Unit/Server/RegistrarTest.php b/tests/Unit/Server/RegistrarTest.php index 3d829d4..a3acccc 100644 --- a/tests/Unit/Server/RegistrarTest.php +++ b/tests/Unit/Server/RegistrarTest.php @@ -197,3 +197,75 @@ public function createAuthorizationCodeGrantClient($name, $redirectUris, $confid 'token_endpoint_auth_method' => 'none', ]); }); + +it('handles oauth registration with allowed domains', function (): void { + if (! class_exists('Laravel\Passport\ClientRepository')) { + // Create a mock ClientRepository class for testing + eval(' + namespace Laravel\Passport; + class ClientRepository { + public function createAuthorizationCodeGrantClient($name, $redirectUris, $confidential, $user, $enableDeviceFlow) { + return (object) [ + "id" => "test-client-id", + "grantTypes" => ["authorization_code"], + "redirectUris" => $redirectUris, + ]; + } + } + '); + } + + $registrar = new Registrar; + $registrar->oauthRoutes(); + + config()->set('mcp.redirect_domains', ['http://localhost:3000/']); + + $this->app->instance('Laravel\Passport\ClientRepository', new \Laravel\Passport\ClientRepository); + + $response = $this->postJson('/oauth/register', [ + 'client_name' => 'Test Client', + 'redirect_uris' => ['http://localhost:3000/callback'], + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'client_id' => 'test-client-id', + 'grant_types' => ['authorization_code'], + 'response_types' => ['code'], + 'redirect_uris' => ['http://localhost:3000/callback'], + 'scope' => 'mcp:use', + 'token_endpoint_auth_method' => 'none', + ]); +}); + +it('handles oauth registration with incorrect redirect domain', function (): void { + if (! class_exists('Laravel\Passport\ClientRepository')) { + // Create a mock ClientRepository class for testing + eval(' + namespace Laravel\Passport; + class ClientRepository { + public function createAuthorizationCodeGrantClient($name, $redirectUris, $confidential, $user, $enableDeviceFlow) { + return (object) [ + "id" => "test-client-id", + "grantTypes" => ["authorization_code"], + "redirectUris" => $redirectUris, + ]; + } + } + '); + } + + $registrar = new Registrar; + $registrar->oauthRoutes(); + + config()->set('mcp.redirect_domains', ['http://allowed-domain.com/']); + + $this->app->instance('Laravel\Passport\ClientRepository', new \Laravel\Passport\ClientRepository); + + $response = $this->postJson('/oauth/register', [ + 'client_name' => 'Test Client', + 'redirect_uris' => ['http://not-allowed.com/callback'], + ]); + + $response->assertStatus(422); +});