fix(frontend/mcp): Filter credential auto-select by server URL discriminator

Prevent MCP credential cross-contamination where a credential for one
server (e.g. Sentry) fills credential fields for other servers (e.g.
Linear). Adds matchesDiscriminatorValues() to match credentials by host
against discriminator_values from the schema.
This commit is contained in:
Zamil Majdy
2026-02-10 07:39:44 +04:00
parent edd9a90903
commit 909f313e1e
2 changed files with 45 additions and 0 deletions

View File

@@ -86,11 +86,13 @@ export function CredentialsGroupedView({
const providerNames = schema.credentials_provider || [];
const credentialTypes = schema.credentials_types || [];
const requiredScopes = schema.credentials_scopes;
const discriminatorValues = schema.discriminator_values;
const savedCredential = findSavedCredentialByProviderAndType(
providerNames,
credentialTypes,
requiredScopes,
allProviders,
discriminatorValues,
);
if (savedCredential) {

View File

@@ -23,6 +23,32 @@ function hasRequiredScopes(
return true;
}
/** Check if a credential matches the discriminator values (e.g. MCP server URL). */
function matchesDiscriminatorValues(
credential: { host?: string | null; provider: string; type: string },
discriminatorValues?: string[],
) {
// MCP OAuth2 credentials must match by server URL
if (credential.type === "oauth2" && credential.provider === "mcp") {
if (!discriminatorValues || discriminatorValues.length === 0) return false;
return (
credential.host != null && discriminatorValues.includes(credential.host)
);
}
// Host-scoped credentials match by host
if (credential.type === "host_scoped" && credential.host) {
if (!discriminatorValues || discriminatorValues.length === 0) return true;
return discriminatorValues.some((v) => {
try {
return new URL(v).hostname === credential.host;
} catch {
return false;
}
});
}
return true;
}
export function splitCredentialFieldsBySystem(
credentialFields: CredentialField[],
allProviders: CredentialsProvidersContextType | null,
@@ -160,6 +186,7 @@ export function findSavedCredentialByProviderAndType(
credentialTypes: string[],
requiredScopes: string[] | undefined,
allProviders: CredentialsProvidersContextType | null,
discriminatorValues?: string[],
): SavedCredential | undefined {
for (const providerName of providerNames) {
const providerData = allProviders?.[providerName];
@@ -176,9 +203,14 @@ export function findSavedCredentialByProviderAndType(
credentialTypes.length === 0 ||
credentialTypes.includes(credential.type);
const scopesMatch = hasRequiredScopes(credential, requiredScopes);
const hostMatches = matchesDiscriminatorValues(
credential,
discriminatorValues,
);
if (!typeMatches) continue;
if (!scopesMatch) continue;
if (!hostMatches) continue;
matchingCredentials.push(credential as SavedCredential);
}
@@ -190,9 +222,14 @@ export function findSavedCredentialByProviderAndType(
credentialTypes.length === 0 ||
credentialTypes.includes(credential.type);
const scopesMatch = hasRequiredScopes(credential, requiredScopes);
const hostMatches = matchesDiscriminatorValues(
credential,
discriminatorValues,
);
if (!typeMatches) continue;
if (!scopesMatch) continue;
if (!hostMatches) continue;
matchingCredentials.push(credential as SavedCredential);
}
@@ -214,6 +251,7 @@ export function findSavedUserCredentialByProviderAndType(
credentialTypes: string[],
requiredScopes: string[] | undefined,
allProviders: CredentialsProvidersContextType | null,
discriminatorValues?: string[],
): SavedCredential | undefined {
for (const providerName of providerNames) {
const providerData = allProviders?.[providerName];
@@ -230,9 +268,14 @@ export function findSavedUserCredentialByProviderAndType(
credentialTypes.length === 0 ||
credentialTypes.includes(credential.type);
const scopesMatch = hasRequiredScopes(credential, requiredScopes);
const hostMatches = matchesDiscriminatorValues(
credential,
discriminatorValues,
);
if (!typeMatches) continue;
if (!scopesMatch) continue;
if (!hostMatches) continue;
matchingCredentials.push(credential as SavedCredential);
}