Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/resource-trailing-slash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/client': patch
---

`auth()` no longer rejects protected resource metadata whose `resource` differs from the server URL only by a trailing slash (for example a server at `https://host/mcp` advertising `https://host/mcp/`). `checkResourceAllowed` now treats a single trailing slash as insignificant in both directions.
8 changes: 2 additions & 6 deletions packages/core-internal/src/shared/authUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export function withoutIssuer(body: unknown): unknown {
/**
* Checks if a requested resource URL matches a configured resource URL.
* A requested resource matches if it has the same scheme, domain, port,
* and its path starts with the configured resource's path.
* and its path starts with the configured resource's path. A single trailing
* slash is not significant, so `/mcp` and `/mcp/` match in either direction.
*
* @param options - The options object
* @param options.requestedResource - The resource URL being requested
Expand All @@ -47,11 +48,6 @@ export function checkResourceAllowed({
return false;
}

// Handle cases like requested=/foo and configured=/foo/
if (requested.pathname.length < configured.pathname.length) {
return false;
}

// Check if the requested path starts with the configured path
// Ensure both paths end with / for proper comparison
// This ensures that if we have paths like "/api" and "/api/users",
Expand Down
19 changes: 17 additions & 2 deletions packages/core-internal/test/shared/authUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,27 @@ describe('auth-utils', () => {
).toBe(true);
});

it('should handle trailing slashes vs no trailing slashes', () => {
it('should treat a single trailing slash as the same resource in both directions', () => {
expect(
checkResourceAllowed({ requestedResource: 'https://example.com/mcp/', configuredResource: 'https://example.com/mcp' })
).toBe(true);
expect(
checkResourceAllowed({ requestedResource: 'https://example.com/folder', configuredResource: 'https://example.com/folder/' })
checkResourceAllowed({ requestedResource: 'https://example.com/mcp', configuredResource: 'https://example.com/mcp/' })
).toBe(true);
});

it('should still reject a shorter path that is not the configured one', () => {
expect(
checkResourceAllowed({
requestedResource: 'https://example.com/folder',
configuredResource: 'https://example.com/folder/sub/'
})
).toBe(false);
expect(
checkResourceAllowed({ requestedResource: 'https://example.com/', configuredResource: 'https://example.com/mcp/' })
).toBe(false);
expect(
checkResourceAllowed({ requestedResource: 'https://example.com/mc', configuredResource: 'https://example.com/mcp/' })
).toBe(false);
});
});
Expand Down
Loading