laravel/mcp: PR #342: Port OAuth client logo and uri to 1.0
Contributed by pushpak1300
Part of the mcp v1.0.1 release
Feature (#2)
Pull request #342 forward-ports #337 from the 0.x branch (shipped in v0.9.5) onto main for the 1.0 line. It adds the RFC 7591 dynamic-registration metadata fields logo_uri and client_uri to MCP's OAuth flow: clients registering at /oauth/register can advertise a logo and a home-page URL, the server stores both when the client table can hold them, and the published mcp::authorize view renders that branding on the consent screen. This was the only v0.9.5 change missing from main — the accompanying app resource domain fix already landed there as 605db234 — and the port arrives as a clean cherry-pick. If you are upgrading from v0.9.6, you have been running this behavior since v0.9.5; nothing changes for you at runtime.
New registration metadata
The register controller gains two optional, independently validated fields:
'logo_uri' => ['nullable', 'string', 'url:http,https', 'max:2048'],
'client_uri' => ['nullable', 'string', 'url:http,https', 'max:2048'],
Both must be http/https URLs of at most 2048 characters. On a successful registration they are echoed back with the usual response:
curl -X POST https://example.test/oauth/register \
-H 'Content-Type: application/json' \
-d '{"client_name": "Acme MCP Client",
"redirect_uris": ["https://app.acme.dev/callback"],
"logo_uri": "https://app.acme.dev/logo.png",
"client_uri": "https://acme.dev"}'
The 201 response now includes logo_uri and client_uri when they were accepted and stored. Invalid values — non-URLs, ftp:// schemes, arrays, or oversized strings — are rejected with a 400 and error: invalid_client_metadata before any client row is created.
Persistence is opt-in and column-aware
No migration ships with this change. The controller introspects the client model's table via the schema builder and only touches columns that exist:
$columns = $client->getConnection()->getSchemaBuilder()->getColumnListing($client->getTable());
$supported = array_intersect(['logo_uri', 'client_uri'], $columns);
To opt in, add nullable columns to your oauth_clients table (or whatever table your Passport client model uses):
Schema::table('oauth_clients', function (Blueprint $table): void {
$table->text('logo_uri')->nullable();
$table->text('client_uri')->nullable();
});
Writes go through the Eloquent model, and the response reads the values back from the saved client, so casts, mutators, and accessors on a custom client model apply — the new tests cover a CustomPassportClient whose mutator lowercases logo_uri, and the API reflects the transformed value. If your client repository doesn't return Eloquent models, or the columns are missing, the metadata is ignored; missing and null values are omitted from the response too, so existing integrations keep working unchanged.
The consent screen gets the client's brand
The published resources/views/mcp/authorize.blade.php renders $client->logo_uri as a 12×12 rounded image in place of the default shield icon, and $client->client_uri as a centered, underlined link (target="_blank" with rel="noopener noreferrer") beneath the scope description. When the metadata is absent, the view falls back to the shield icon exactly as before, and it tolerates models without those attributes, so older published views and minimal client records won't break the screen.
Sharper redirect URI errors
The PR also tightens error classification on redirect_uris. The rule set now uses bail, and the controller classifies failures with $errors->first('redirect_uris*'). Two visible improvements: a non-string entry such as [42] in redirect_uris now correctly reports invalid_redirect_uri instead of invalid_client_metadata, and redirect errors take precedence when both redirect URIs and metadata are invalid — a bad logo_uri can no longer mask a bad callback URL. The error response shape itself is unchanged.
Upgrade impact
Coming from v0.9.6, this PR is a no-op at runtime: the identical behavior shipped in v0.9.5, and the 1.0 port is a clean cherry-pick — lint, PHPStan, and the ported RegistrarTest all pass. The change matters if you track main or jump to 1.0 from a release older than v0.9.5. To use the feature, add the two nullable columns shown above and, if you have customized it, republish the mcp::authorize view. Clients should not assume the metadata keys appear in every registration response — when the columns are missing the fields are simply omitted, which is the RFC 7591-friendly behavior.
Takeaways
/oauth/registeraccepts optionallogo_uriandclient_uri(http/https URLs, max 2048 characters) and echoes them on success.- Values are persisted — and returned — only when the client table has matching columns; add them to opt in.
- The
mcp::authorizeview shows the client logo (replacing the shield icon) and a website link when the metadata is present. - Invalid metadata fails with
invalid_client_metadatabefore a client is created; redirect URI problems keep their distinctinvalid_redirect_urierror and take precedence over metadata errors. - Upgrading from v0.9.6? You already have this — the 1.0 port is pure behavioral parity, no action required.