diff --git a/.changeset/brave-suits-pay.md b/.changeset/brave-suits-pay.md new file mode 100644 index 0000000000..84bfdbf964 --- /dev/null +++ b/.changeset/brave-suits-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Fix OAuth 2.0 Protected Resource Metadata endpoint returning internal plugin URL, preventing some MCP clients like Claude Code from authenticating diff --git a/plugins/mcp-actions-backend/src/plugin.test.ts b/plugins/mcp-actions-backend/src/plugin.test.ts index 2c03800915..f42dbbd738 100644 --- a/plugins/mcp-actions-backend/src/plugin.test.ts +++ b/plugins/mcp-actions-backend/src/plugin.test.ts @@ -307,10 +307,17 @@ describe('Mcp Backend', () => { }); it('should expose oauth-protected-resource when DCR is enabled', async () => { + const mockExternalBaseUrl = 'http://external.local:0/api'; + const mockDiscovery = mockServices.discovery.mock({ + getExternalBaseUrl: async pluginId => + `${mockExternalBaseUrl}/${pluginId}`, + }); + const { server } = await startTestBackend({ features: [ mcpPlugin, mockPluginWithActions, + mockDiscovery.factory, mockServices.rootConfig.factory({ data: { backend: { @@ -335,6 +342,10 @@ describe('Mcp Backend', () => { expect(response.body.resource).toMatch(/\/api\/mcp-actions$/); expect(response.body.authorization_servers).toHaveLength(1); expect(response.body.authorization_servers[0]).toMatch(/\/api\/auth$/); + expect(response.body.resource).toContain(`${mockExternalBaseUrl}`); + expect(response.body.authorization_servers[0]).toContain( + `${mockExternalBaseUrl}/`, + ); }); it('should expose oauth-protected-resource when CIMD is enabled', async () => { diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index f8844249f6..3055459cc9 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -144,8 +144,8 @@ export const mcpPlugin = createBackendPlugin({ '/.well-known/oauth-protected-resource', async (_, res) => { const [authBaseUrl, mcpBaseUrl] = await Promise.all([ - discovery.getBaseUrl('auth'), - discovery.getBaseUrl('mcp-actions'), + discovery.getExternalBaseUrl('auth'), + discovery.getExternalBaseUrl('mcp-actions'), ]); res.json({ resource: mcpBaseUrl,