Skip to content
LaravelBlog

laravel/mcp: Fix handling of trailing slash in discovery urls

LaravelBlogBot
LaravelBlogBot

Contributed by jimbojsb

Part of the mcp v1.0.2 release

<!-- Category: fix (3) -->

PR #354 is a small, surgical bug fix to the OAuth client's authorization server discovery. When a protected resource metadata document advertises an authorization server whose issuer URL ends in a trailing slash — as FirefliesAI's MCP server does — the discovery code treated that slash as a real path segment and built its metadata candidate URLs around it. None of those candidates resolved, so the OAuth flow died before the user ever reached a login screen. After this fix, trailing slashes are normalized away and both issuer styles resolve to the same candidate URLs. There are no API, config, or schema changes, so upgrading from v1.0.1 is a drop-in affair.

What changed

The entire fix is one line in src/Client/OAuth/AuthServerDiscovery.php, inside the metadataUrls() method that turns an issuer into a list of .well-known/oauth-authorization-server candidate URLs:

// Before
$path = $parts['path'] ?? '';

// After
$path = rtrim($parts['path'] ?? '', '/');

The method parses the issuer into an origin plus a path. An empty path means "use the root-level well-known URLs"; a non-empty path means "build path-qualified candidates". Previously, an issuer like https://auth.test/ parsed to a path of "/", which counted as non-empty and sent discovery down the path-qualified branch with a meaningless segment baked in. The fix strips trailing slashes before that check, so "/" is now correctly treated as "no path".

A regression test was added to tests/Unit/Client/OAuth/OAuthClientTest.php covering exactly this shape, and no existing tests were altered.

Why it matters

When your client connects to an MCP server protected by OAuth, discovery runs first: the client fetches /.well-known/oauth-protected-resource/mcp, reads the authorization_servers array, and then fetches each listed authorization server's metadata at its well-known URL. Only after that metadata resolves can the client build the redirect to the authorization endpoint.

Whether an issuer URL ends in a slash is largely a matter of convention — a server that meets the MCP spec can publish either form. The PR's motivating example is FirefliesAI's MCP server, which publishes its issuer with the slash. Under v1.0.1, connecting to such a server through the OAuth client failed at discovery, not because the server was wrong, but because the client was over-interpreting a stylistic trailing slash. Since servers like this are out in the wild and spec-compliant, the client has to tolerate them.

Before and after

Consider a protected resource metadata response shaped like this:

{
    "authorization_servers": ["https://auth.test/"]
}

Before the fix: the path "/" was treated as meaningful, so the candidate metadata URLs were constructed with that stray segment in place and pointed at locations the authorization server does not serve. Discovery exhausted its candidates and the OAuth flow aborted.

After the fix: the issuer is normalized to https://auth.test, the root-level well-known URL https://auth.test/.well-known/oauth-authorization-server resolves, and the flow completes. The new unit test exercises precisely this path:

use Laravel\Mcp\Client\Client;

$target = Client::web('https://mcp.test/mcp')
    ->withOAuth(clientId: 'client-123', redirectUri: 'https://app.test/callback')
    ->oAuthClient()
    ->redirect()
    ->getTargetUrl();

// $target starts with https://auth.test/authorize?...

The test fakes the protected resource metadata with a trailing-slash issuer, fakes the authorization server metadata at the root well-known URL, and asserts that the redirect target is the authorization endpoint with the OAuth query parameters attached — the exact flow that previously broke.

Upgrade impact

For most applications, this fix is invisible: pull the new version and move on.

  • No code changes required. The change lives entirely inside AuthServerDiscovery; no public API, configuration, or environment variables changed.
  • Remove workarounds if you had them. If you normalized issuer URLs (e.g. with rtrim($issuer, '/')) before handing them to the OAuth client, or carried a patched copy of the discovery class, you can safely delete that code.
  • No regression risk. rtrim(..., '/') is a no-op for paths that do not end in a slash, so issuers published without a trailing slash behave exactly as they did in v1.0.1. Only the previously broken case changes.
  • Retry previously failing connections. If you were blocked from connecting to FirefliesAI's or a similar MCP server via OAuth, upgrade and try again — no change is needed on the server side, since those servers were already spec-compliant.

Key takeaways

  • PR #354 fixes OAuth authorization server discovery for issuer URLs that end in a trailing slash.
  • The trigger is real-world: FirefliesAI's MCP server publishes a trailing-slash issuer, and both forms are valid under the spec.
  • The fix is one normalization line (rtrim) plus a regression test; nothing about the public API changed.
  • Upgrading from v1.0.1 requires no code changes — and if you added slash-stripping workarounds, you can now remove them.

Sources

More from this release

Related Articles