diff --git a/api/application.yaml b/api/application.yaml
index 89ebb81a6f..2868264dc5 100644
--- a/api/application.yaml
+++ b/api/application.yaml
@@ -803,7 +803,7 @@ components:
example: "770e8400-e29b-41d4-a716-446655440002"
type:
type: string
- enum: [browser, fullstack, mobile, m2m, custom]
+ enum: [browser, fullstack, mobile, m2m, mcp, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
@@ -878,10 +878,11 @@ components:
type: string
description: >
Flow Secret used to authenticate when initiating a flow directly via the Flow Execution API.
- Issued only to full-stack and custom applications that either have no OAuth 2.0 configuration
- (embedded) or are configured as a confidential, non-redirect OAuth 2.0 client. Browser, mobile,
- and machine-to-machine applications are never issued one. When omitted on creation, a secret is
- generated automatically for eligible applications and returned once in the creation response.
+ Issued only to full-stack, custom, and mcp applications that either have no OAuth 2.0
+ configuration (embedded) or are configured as a confidential, non-redirect OAuth 2.0 client.
+ Browser, mobile, and machine-to-machine applications are never issued one. When omitted
+ on creation, a secret is generated automatically for eligible applications and returned once in
+ the creation response.
example: "550e8400e29b41d4a716446655440000abcdef0123456789"
ApplicationCompleteResponse:
@@ -939,7 +940,7 @@ components:
example: "660e8400-e29b-41d4-a716-446655440001"
type:
type: string
- enum: [browser, fullstack, mobile, m2m, custom]
+ enum: [browser, fullstack, mobile, m2m, mcp, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
@@ -952,10 +953,10 @@ components:
type: string
description: >
Flow Secret used to authenticate when initiating a flow directly via the Flow Execution API.
- Issued and returned once in the creation response only for full-stack and custom applications
- that either have no OAuth 2.0 configuration (embedded) or are configured as a confidential,
- non-redirect OAuth 2.0 client. Browser, mobile, and machine-to-machine applications are never
- issued one.
+ Issued and returned once in the creation response only for full-stack, custom, and mcp
+ applications that either have no OAuth 2.0 configuration (embedded) or are configured as a
+ confidential, non-redirect OAuth 2.0 client. Browser, mobile, and machine-to-machine
+ applications are never issued one.
example: "550e8400e29b41d4a716446655440000abcdef0123456789"
url:
type: string
@@ -1075,7 +1076,7 @@ components:
example: "770e8400-e29b-41d4-a716-446655440002"
type:
type: string
- enum: [browser, fullstack, mobile, m2m, custom]
+ enum: [browser, fullstack, mobile, m2m, mcp, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
@@ -1203,7 +1204,7 @@ components:
example: "770e8400-e29b-41d4-a716-446655440002"
type:
type: string
- enum: [browser, fullstack, mobile, m2m, custom]
+ enum: [browser, fullstack, mobile, m2m, mcp, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
diff --git a/backend/internal/application/application_type_test.go b/backend/internal/application/application_type_test.go
index 92ffe0d2d7..c230e7327a 100644
--- a/backend/internal/application/application_type_test.go
+++ b/backend/internal/application/application_type_test.go
@@ -116,9 +116,10 @@ func (s *ApplicationTypeTestSuite) TestFlowSecretIneligibleByType() {
}
}
-// TestFullStackAndCustomFlowSecretEligibility verifies full-stack and custom apps derive eligibility
-// from the OAuth config shape: only confidential, non-redirect (embedded) clients are eligible.
-func (s *ApplicationTypeTestSuite) TestFullStackAndCustomFlowSecretEligibility() {
+// TestFullStackCustomAndMCPFlowSecretEligibility verifies full-stack, custom, and mcp apps derive
+// eligibility from the OAuth config shape: an app with no OAuth configuration (embedded), or a
+// confidential, non-redirect client, is eligible; a redirect or m2m-shaped client is not.
+func (s *ApplicationTypeTestSuite) TestFullStackCustomAndMCPFlowSecretEligibility() {
embedded := &providers.InboundAuthConfigWithSecret{
Type: providers.OAuthInboundAuthType,
OAuthConfig: &providers.OAuthConfigWithSecret{
@@ -145,6 +146,7 @@ func (s *ApplicationTypeTestSuite) TestFullStackAndCustomFlowSecretEligibility()
for _, appType := range []model.ApplicationType{
model.ApplicationTypeFullStack,
model.ApplicationTypeCustom,
+ model.ApplicationTypeMCP,
} {
// Embedded app with no OAuth config, and confidential non-redirect app, are eligible.
s.True(isFlowSecretEligible(appType, nil), "type %q embedded should be eligible", appType)
diff --git a/backend/internal/application/error_constants.go b/backend/internal/application/error_constants.go
index 82684db622..0b97540e6e 100644
--- a/backend/internal/application/error_constants.go
+++ b/backend/internal/application/error_constants.go
@@ -532,7 +532,7 @@ var (
ErrorDescription: tidcommon.I18nMessage{
Key: "error.applicationservice.invalid_application_type_description",
DefaultValue: "The provided application type is not supported. It must be one of: " +
- "browser, fullstack, mobile, m2m, custom.",
+ "browser, fullstack, mobile, m2m, mcp, custom.",
},
}
// ErrorApplicationTypeImmutable is returned when an update attempts to change the application type.
@@ -559,7 +559,7 @@ var (
ErrorDescription: tidcommon.I18nMessage{
Key: "error.applicationservice.application_type_required_description",
DefaultValue: "An application type must be provided. It must be one of: " +
- "browser, fullstack, mobile, m2m, custom.",
+ "browser, fullstack, mobile, m2m, mcp, custom.",
},
}
)
diff --git a/backend/internal/application/model/application.go b/backend/internal/application/model/application.go
index cfcc081848..c8db446d86 100644
--- a/backend/internal/application/model/application.go
+++ b/backend/internal/application/model/application.go
@@ -33,9 +33,9 @@ type ApplicationDTO struct {
OUHandle string `json:"ouHandle,omitempty" jsonschema:"Organization unit handle. Resolved to an ID by the service layer."`
Name string `json:"name" jsonschema:"Application name."`
Description string `json:"description,omitempty" jsonschema:"Optional description of the application's purpose or functionality."`
- Type ApplicationType `json:"type,omitempty" jsonschema:"Application type. Canonical platform/client class: browser, fullstack, mobile, m2m, or custom. Required at creation and immutable."`
+ Type ApplicationType `json:"type,omitempty" jsonschema:"Application type. Canonical platform/client class: browser, fullstack, mobile, m2m, mcp, or custom. Required at creation and immutable."`
Template string `json:"template,omitempty" jsonschema:"Application template. Optional. Display metadata identifying the frontend template used to create the application."`
- FlowSecret string `json:"flowSecret,omitempty" jsonschema:"Flow Secret used to authenticate when initiating a flow directly via the Flow Execution API. Issued once on creation only to full-stack and custom applications that either have no OAuth 2.0 configuration (embedded) or are configured as a confidential, non-redirect OAuth 2.0 client. Browser, mobile, and m2m applications are never issued one."`
+ FlowSecret string `json:"flowSecret,omitempty" jsonschema:"Flow Secret used to authenticate when initiating a flow directly via the Flow Execution API. Issued once on creation only to full-stack, custom, and mcp applications that either have no OAuth 2.0 configuration (embedded) or are configured as a confidential, non-redirect OAuth 2.0 client. Browser, mobile, and m2m applications are never issued one."`
URL string `json:"url,omitempty" jsonschema:"Application home URL. Optional. The main URL where your application is hosted."`
LogoURL string `json:"logoUrl,omitempty" jsonschema:"Logo image URL. Optional. Displayed in login pages and application listings."`
@@ -187,7 +187,7 @@ type BasicApplicationResponse struct {
IsSignOutFlowEnabled bool `json:"isSignOutFlowEnabled" jsonschema:"Sign-out enabled status."`
ThemeID string `json:"themeId,omitempty" jsonschema:"Theme ID."`
LayoutID string `json:"layoutId,omitempty" jsonschema:"Layout ID."`
- Type ApplicationType `json:"type,omitempty" jsonschema:"Application type (browser, fullstack, mobile, m2m, custom)."`
+ Type ApplicationType `json:"type,omitempty" jsonschema:"Application type (browser, fullstack, mobile, m2m, mcp, custom)."`
Template string `json:"template,omitempty" jsonschema:"Application Template."`
IsReadOnly bool `json:"isReadOnly" jsonschema:"Indicates if the application is read-only (declarative/immutable)."`
}
diff --git a/backend/internal/application/model/constants.go b/backend/internal/application/model/constants.go
index d35d79b590..b33023afc4 100644
--- a/backend/internal/application/model/constants.go
+++ b/backend/internal/application/model/constants.go
@@ -33,6 +33,7 @@ const (
ApplicationTypeFullStack ApplicationType = "fullstack"
ApplicationTypeMobile ApplicationType = "mobile"
ApplicationTypeM2M ApplicationType = "m2m"
+ ApplicationTypeMCP ApplicationType = "mcp"
ApplicationTypeCustom ApplicationType = "custom"
)
diff --git a/backend/internal/application/model/utils.go b/backend/internal/application/model/utils.go
index 8edc6037ed..8b2e1570ed 100644
--- a/backend/internal/application/model/utils.go
+++ b/backend/internal/application/model/utils.go
@@ -22,7 +22,7 @@ package model
func IsValidApplicationType(t ApplicationType) bool {
switch t {
case ApplicationTypeBrowser, ApplicationTypeFullStack, ApplicationTypeMobile,
- ApplicationTypeM2M, ApplicationTypeCustom:
+ ApplicationTypeM2M, ApplicationTypeMCP, ApplicationTypeCustom:
return true
default:
return false
diff --git a/backend/internal/application/service.go b/backend/internal/application/service.go
index 64c7fc50ff..1f72f119a2 100644
--- a/backend/internal/application/service.go
+++ b/backend/internal/application/service.go
@@ -546,9 +546,9 @@ func (as *applicationService) updateEntityDataForApplicationUpdate(ctx context.C
}
// isFlowSecretEligible reports whether an application may hold a Flow Secret. Browser (public
-// redirect), mobile (attestation), and m2m (direct token) apps never hold one. Full-stack and custom
-// apps derive eligibility from the OAuth config shape: only confidential, non-redirect clients (or
-// embedded apps with no OAuth config) are eligible.
+// redirect), mobile (attestation), and m2m (direct token) apps never hold one. Full-stack, custom,
+// and mcp apps derive eligibility from the OAuth config shape: only confidential, non-redirect
+// clients (or embedded apps with no OAuth config) are eligible.
func isFlowSecretEligible(appType model.ApplicationType,
inboundAuthConfig *providers.InboundAuthConfigWithSecret) bool {
switch appType {
@@ -1176,7 +1176,8 @@ func (as *applicationService) validateApplicationFields(
// immutability (update) are handled by the respective callers.
switch app.Type {
case "", model.ApplicationTypeBrowser, model.ApplicationTypeFullStack,
- model.ApplicationTypeMobile, model.ApplicationTypeM2M, model.ApplicationTypeCustom:
+ model.ApplicationTypeMobile, model.ApplicationTypeM2M, model.ApplicationTypeMCP,
+ model.ApplicationTypeCustom:
default:
return &ErrorInvalidApplicationType
}
diff --git a/backend/internal/flow/flowexec/service.go b/backend/internal/flow/flowexec/service.go
index 6d936dc829..7befefb258 100644
--- a/backend/internal/flow/flowexec/service.go
+++ b/backend/internal/flow/flowexec/service.go
@@ -331,7 +331,7 @@ func (s *flowExecService) resolveFlowInitiationMode(
}
return flowInitiationAttestation, client.Attestation, nil
default:
- // Full-stack and custom apps may be embedded or redirect-based; derive from the OAuth profile.
+ // Full-stack, custom, and mcp apps may be embedded or redirect-based; derive from the OAuth profile.
return s.resolveFlowInitiationModeFromProfile(ctx, appID)
}
}
diff --git a/backend/internal/flow/flowexec/service_test.go b/backend/internal/flow/flowexec/service_test.go
index a068482f23..b0348d9fa9 100644
--- a/backend/internal/flow/flowexec/service_test.go
+++ b/backend/internal/flow/flowexec/service_test.go
@@ -2417,7 +2417,7 @@ func (s *ServiceTestSuite) TestSetApplicationToContext_BuildApplicationError() {
// The flow-initiation mode is resolved from the application type. Machine-to-machine, browser, and
// mobile (without attestation) apps may not initiate a flow directly and never consult the OAuth
-// profile. Full-stack and custom apps derive the mode from their profile.
+// profile. Full-stack, custom, and mcp apps derive the mode from their profile.
func (s *ServiceTestSuite) TestResolveFlowInitiationMode_ByType() {
const appID = "test-app"
tokenExchange := string(providers.GrantTypeTokenExchange)
@@ -2445,6 +2445,16 @@ func (s *ServiceTestSuite) TestResolveFlowInitiationMode_ByType() {
attestation: &providers.AttestationConfig{Apple: &providers.AppleAttestationConfig{}},
expectMode: flowInitiationAttestation,
},
+ {
+ name: "mcp embedded uses flow secret", appType: model.ApplicationTypeMCP,
+ profile: &providers.OAuthProfile{GrantTypes: []string{"client_credentials", tokenExchange}},
+ expectMode: flowInitiationFlowSecret,
+ },
+ {
+ name: "mcp redirect not permitted", appType: model.ApplicationTypeMCP,
+ profile: &providers.OAuthProfile{GrantTypes: []string{"authorization_code"}},
+ expectMode: flowInitiationNotPermitted,
+ },
{
name: "fullstack redirect not permitted", appType: model.ApplicationTypeFullStack,
profile: &providers.OAuthProfile{GrantTypes: []string{"authorization_code"}},
@@ -2489,8 +2499,9 @@ func (s *ServiceTestSuite) TestResolveFlowInitiationMode_ByType() {
}
mockActorProvider.EXPECT().GetInboundClientByID(mock.Anything, appID).Return(client, nil)
- // The OAuth profile is consulted for full-stack and custom apps.
- if tc.appType == model.ApplicationTypeFullStack || tc.appType == model.ApplicationTypeCustom {
+ // The OAuth profile is consulted for full-stack, custom, and mcp apps.
+ if tc.appType == model.ApplicationTypeFullStack || tc.appType == model.ApplicationTypeCustom ||
+ tc.appType == model.ApplicationTypeMCP {
mockActorProvider.EXPECT().GetOAuthProfileByID(mock.Anything, appID).Return(tc.profile, tc.profileErr)
}
diff --git a/backend/internal/system/i18n/core/defaults.go b/backend/internal/system/i18n/core/defaults.go
index 1a8be25a4e..fcf3dc9399 100644
--- a/backend/internal/system/i18n/core/defaults.go
+++ b/backend/internal/system/i18n/core/defaults.go
@@ -154,7 +154,7 @@ var defaultMessages = map[string]string{
"error.applicationservice.application_type_immutable": "Application type cannot be changed",
"error.applicationservice.application_type_immutable_description": "The application type is set at creation and cannot be modified.",
"error.applicationservice.application_type_required": "Application type is required",
- "error.applicationservice.application_type_required_description": "An application type must be provided. It must be one of: browser, fullstack, mobile, m2m, custom.",
+ "error.applicationservice.application_type_required_description": "An application type must be provided. It must be one of: browser, fullstack, mobile, m2m, mcp, custom.",
"error.applicationservice.application_with_client_id_already_exists": "Application with client ID already exists",
"error.applicationservice.application_with_client_id_already_exists_description": "An application with the same client ID already exists",
"error.applicationservice.auth_code_requires_code_response_type_description": "authorization_code grant type requires 'code' response type",
@@ -185,7 +185,7 @@ var defaultMessages = map[string]string{
"error.applicationservice.invalid_application_name": "Invalid application name",
"error.applicationservice.invalid_application_name_description": "The provided application name is invalid or empty",
"error.applicationservice.invalid_application_type": "Invalid application type",
- "error.applicationservice.invalid_application_type_description": "The provided application type is not supported. It must be one of: browser, fullstack, mobile, m2m, custom.",
+ "error.applicationservice.invalid_application_type_description": "The provided application type is not supported. It must be one of: browser, fullstack, mobile, m2m, mcp, custom.",
"error.applicationservice.invalid_application_url": "Invalid application URL",
"error.applicationservice.invalid_application_url_description": "The provided application URL is not a valid URI",
"error.applicationservice.invalid_auth_flow_id": "Invalid auth flow ID",
diff --git a/backend/pkg/thunderidengine/providers/model.go b/backend/pkg/thunderidengine/providers/model.go
index c724d2c13d..016836e6ec 100644
--- a/backend/pkg/thunderidengine/providers/model.go
+++ b/backend/pkg/thunderidengine/providers/model.go
@@ -1004,7 +1004,7 @@ type Application struct {
OUID string `yaml:"ouId,omitempty" json:"ouId,omitempty" jsonschema:"Organization unit ID. The OU this application belongs to."`
Name string `yaml:"name,omitempty" json:"name,omitempty" jsonschema:"Application name."`
Description string `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"Optional description of the application's purpose."`
- Type string `yaml:"type,omitempty" json:"type,omitempty" jsonschema:"Application type (browser, fullstack, mobile, m2m, custom)."`
+ Type string `yaml:"type,omitempty" json:"type,omitempty" jsonschema:"Application type (browser, fullstack, mobile, m2m, mcp, custom)."`
Template string `yaml:"template,omitempty" json:"template,omitempty" jsonschema:"Template used to create the application."`
URL string `yaml:"url,omitempty" json:"url,omitempty" jsonschema:"Application home URL."`
diff --git a/docs/content/guides/applications/application-settings.mdx b/docs/content/guides/applications/application-settings.mdx
index 404ece3dfd..0ee686ae89 100644
--- a/docs/content/guides/applications/application-settings.mdx
+++ b/docs/content/guides/applications/application-settings.mdx
@@ -66,7 +66,7 @@ On the **Customization** tab, control how your application looks and what legal
## Configure What Goes Into Tokens
:::note
-This section applies to OAuth 2.0 / OIDC applications (Browser App, Full-stack App, Mobile App, and Backend Service). For applications that use the embedded sign-in approach, user attributes are configured in the assertion returned by the flow, not in tokens.
+This section applies to OAuth 2.0 / OIDC applications (Browser App, Full-stack App, Mobile App, Backend Service, MCP Client, and Custom App). For applications that use the embedded sign-in approach, user attributes are configured in the assertion returned by the flow, not in tokens.
:::
On the **Token** tab, configure what your application's tokens carry and how long they stay valid. The tab has two sub-tabs:
@@ -113,7 +113,7 @@ On the **Application** sub-tab, configure the access token the application recei
## Rotate the Client Secret
:::note
-Client secrets are only present on confidential clients, Full-stack App and Backend Service application types. This section does not apply to public clients such as Browser App or Mobile App.
+Client secrets are only present on confidential clients: Full-stack App and Backend Service application types, and MCP Client or Custom App applications configured as confidential. This section does not apply to public clients such as Browser App, Mobile App, or an MCP Client or Custom App configured as public.
:::
If you need to invalidate the current client secret, open the General tab and click **Regenerate Client Secret** in the **Danger Zone**.
@@ -131,7 +131,7 @@ This section applies to OAuth 2.0 / OIDC applications only.
The **Advanced Settings** tab shows the OAuth 2.0 settings configured at creation.
| Setting | Description |
|---------|-------------|
-| **Grant Types** | The OAuth 2.0 flows this application can use. Supported values: `authorization_code`, `refresh_token`, `client_credentials`, `urn:ietf:params:oauth:grant-type:token-exchange`, `urn:ietf:params:oauth:grant-type:jwt-bearer`. Whether this application can initiate flows directly via `POST /flow/execute` depends on its [application type](../manage-applications#application-types) and, for Full-stack and Custom applications, its grant configuration. See [Direct Initiation Restriction](../../key-concepts/authentication/integration-models.mdx#app-native) for the full rule. |
+| **Grant Types** | The OAuth 2.0 flows this application can use. Supported values: `authorization_code`, `refresh_token`, `client_credentials`, `urn:ietf:params:oauth:grant-type:token-exchange`, `urn:ietf:params:oauth:grant-type:jwt-bearer`. Whether this application can initiate flows directly via `POST /flow/execute` depends on its [application type](../manage-applications#application-types) and, for Full-stack, Custom, and MCP Client applications, its grant configuration. See [Direct Initiation Restriction](../../key-concepts/authentication/integration-models.mdx#app-native) for the full rule. |
| **Response Types** | The response types the application can request (for example, `code`). |
| **Token Endpoint Auth Method** | How the application authenticates at the token endpoint: `client_secret_basic` (default), `client_secret_post`, `private_key_jwt`, or `none` (public clients). |
| **Public Client** | Whether this is a public client that cannot securely store a client secret. |
diff --git a/docs/content/guides/applications/manage-applications.mdx b/docs/content/guides/applications/manage-applications.mdx
index a282256383..c23569cd79 100644
--- a/docs/content/guides/applications/manage-applications.mdx
+++ b/docs/content/guides/applications/manage-applications.mdx
@@ -22,7 +22,7 @@ OAuth 2.0 applications additionally hold:
## Application Types
- provides five application types. Choose the one that matches your architecture.
+ provides six application types. Choose the one that matches your architecture.
| Type | Description | Client Secret |
|------|-------------|---------------|
@@ -30,11 +30,12 @@ OAuth 2.0 applications additionally hold:
| **Full-stack App** | For server-rendered web applications (Express, Spring, .NET) where the backend handles the token exchange. | Yes - confidential client |
| **Mobile App** | For native or cross-platform mobile apps (Swift, Kotlin, React Native, Flutter).| No - public client |
| **Backend Service** | For server-to-server communication with no user interaction (background jobs, internal APIs, microservices). | Yes - confidential client |
+| **MCP Client** | For AI applications that connect to MCP servers using the Model Context Protocol. It may be a public or confidential client depending on its OAuth 2.0 configuration. | Depends on configuration |
| **Custom App** | For applications that do not fit the other types, with full control over configuration. | Depends on configuration |
The application type determines whether issues a Flow Secret and whether the application can call the Flow Execution API directly to sign users in. See [Integration Models](../../key-concepts/authentication/integration-models.mdx#app-native) for how each type behaves.
-You set the application type when you create the application, and you cannot change it afterward. If you're creating applications through the REST API or a declarative resource instead of the Console, provide the `type` attribute with one of `browser`, `fullstack`, `mobile`, `m2m`, or `custom`; the API rejects a create request that omits it.
+You set the application type when you create the application, and you cannot change it afterward. If you're creating applications through the REST API or a declarative resource instead of the Console, provide the `type` attribute with one of `browser`, `fullstack`, `mobile`, `m2m`, `mcp`, or `custom`; the API rejects a create request that omits it.
## Create an Application
diff --git a/docs/content/key-concepts/authentication/integration-models.mdx b/docs/content/key-concepts/authentication/integration-models.mdx
index eb4ea31ea4..045058c26d 100644
--- a/docs/content/key-concepts/authentication/integration-models.mdx
+++ b/docs/content/key-concepts/authentication/integration-models.mdx
@@ -62,7 +62,7 @@ Whether an application may initiate one of these flows directly depends on its [
- **Browser** and **Backend Service (m2m)** applications can never initiate these flows directly. A browser application must sign users in through the [Authorization endpoint](../../../apis#tag/authorization) (`GET /oauth2/authorize`), which drives the flow internally. A Backend Service application authenticates itself with the `client_credentials` grant instead of signing in a user.
- **Mobile** applications must present a valid platform attestation token. See [Attestation](#attestation).
-- **Full-stack** and **Custom** applications are derived from their OAuth 2.0 configuration. An application with no OAuth 2.0 configuration, or one that is confidential and does not use the `authorization_code` grant, authenticates with a Flow Secret. An application configured with the `authorization_code` grant, or with `client_credentials` as its only grant, cannot initiate the flow directly.
+- **Full-stack**, **Custom**, and **MCP Client** applications are derived from their OAuth 2.0 configuration. An application with no OAuth 2.0 configuration, or one that is confidential, does not use the `authorization_code` grant, and is not configured with `client_credentials` as its only grant, authenticates with a Flow Secret. An application configured with the `authorization_code` grant, or with `client_credentials` as its only grant, cannot initiate the flow directly.
:::
### Key Features
diff --git a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx
index aad89ad872..80e356b104 100644
--- a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx
+++ b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/EditGeneralSettings.tsx
@@ -27,7 +27,7 @@ import QuickCopySection from './QuickCopySection';
import type {Application} from '../../../models/application';
import {TokenEndpointAuthMethods} from '../../../models/oauth';
import type {OAuth2Config} from '../../../models/oauth';
-import resolveApplicationType from '../../../utils/resolveApplicationType';
+import resolveApplicationType, {isClientCredentialsOnlyGrantSet} from '../../../utils/resolveApplicationType';
import ApplicationDeleteDialog from '../../ApplicationDeleteDialog';
import ClientSecretSuccessDialog from '../../ClientSecretSuccessDialog';
import RegenerateFlowSecretDialog from '../../RegenerateFlowSecretDialog';
@@ -118,15 +118,19 @@ export default function EditGeneralSettings({
oauth2Config?.tokenEndpointAuthMethod === TokenEndpointAuthMethods.CLIENT_SECRET_BASIC ||
oauth2Config?.tokenEndpointAuthMethod === TokenEndpointAuthMethods.CLIENT_SECRET_POST;
- // Only flow-native apps are issued a Flow Secret and can rotate it: full-stack or custom apps
- // using the embedded (non-redirect) sign-in option. Browser (public redirect), mobile
- // (attestation), and m2m (direct token) apps never hold one. The canonical application type is the
- // discriminator, falling back to the OAuth config shape for legacy/custom apps.
+ // Only flow-native apps are issued a Flow Secret and can rotate it: full-stack, custom, or mcp
+ // apps using the embedded (non-redirect) sign-in option. Browser (public redirect), mobile
+ // (attestation), and m2m (direct token, including client_credentials-only mcp configs) apps never
+ // hold one. The canonical application type is the discriminator, falling back to the OAuth config
+ // shape for legacy/custom apps.
const resolvedType = resolveApplicationType(application.type, oauth2Config);
const grantTypes = oauth2Config?.grantTypes ?? [];
const isFlowNativeClient =
- (resolvedType === 'fullstack' || resolvedType === 'custom') &&
- (!oauth2Config || (!oauth2Config.publicClient && !grantTypes.includes('authorization_code')));
+ (resolvedType === 'fullstack' || resolvedType === 'custom' || resolvedType === 'mcp') &&
+ (!oauth2Config ||
+ (!oauth2Config.publicClient &&
+ !grantTypes.includes('authorization_code') &&
+ !isClientCredentialsOnlyGrantSet(grantTypes)));
const handleRegenerateClick = useCallback((): void => {
setRegenerateDialogOpen(true);
diff --git a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx
index 4c8f830b6c..bac1a28629 100644
--- a/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx
+++ b/frontend/apps/console/src/features/applications/components/edit-application/general-settings/__tests__/EditGeneralSettings.test.tsx
@@ -467,6 +467,52 @@ describe('EditGeneralSettings', () => {
expect(screen.getByTestId('regenerate-flow-secret-button')).toBeInTheDocument();
});
+ it('should show regenerate Flow Secret for an mcp type app configured as confidential', () => {
+ const mcpApplication: Application = {...mockApplication, type: 'mcp'} as Application;
+ const flowNativeConfig: OAuth2Config = {
+ clientId: 'mcp-flow-native-123',
+ tokenEndpointAuthMethod: 'client_secret_basic',
+ publicClient: false,
+ grantTypes: ['client_credentials', 'urn:ietf:params:oauth:grant-type:token-exchange'],
+ } as OAuth2Config;
+
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId('regenerate-flow-secret-button')).toBeInTheDocument();
+ });
+
+ it('should not show regenerate Flow Secret for an mcp type app with a client_credentials-only config', () => {
+ const mcpApplication: Application = {...mockApplication, type: 'mcp'} as Application;
+ const m2mShapedConfig: OAuth2Config = {
+ clientId: 'mcp-m2m-123',
+ tokenEndpointAuthMethod: 'client_secret_basic',
+ publicClient: false,
+ grantTypes: ['client_credentials'],
+ } as OAuth2Config;
+
+ render(
+ ,
+ );
+
+ expect(screen.queryByTestId('regenerate-flow-secret-button')).not.toBeInTheDocument();
+ });
+
it('should not show regenerate Flow Secret for an M2M (client_credentials only) app', () => {
const m2mConfig: OAuth2Config = {
clientId: 'm2m-123',
diff --git a/frontend/apps/console/src/features/applications/components/edit-application/mcp/McpConnectTab.tsx b/frontend/apps/console/src/features/applications/components/edit-application/mcp/McpConnectTab.tsx
index c5e49d551b..3554be8c52 100644
--- a/frontend/apps/console/src/features/applications/components/edit-application/mcp/McpConnectTab.tsx
+++ b/frontend/apps/console/src/features/applications/components/edit-application/mcp/McpConnectTab.tsx
@@ -26,7 +26,7 @@ import McpAccessSection from './McpAccessSection';
import type {Application} from '../../../models/application';
import {TokenEndpointAuthMethods} from '../../../models/oauth';
import type {OAuth2Config} from '../../../models/oauth';
-import resolveApplicationType from '../../../utils/resolveApplicationType';
+import resolveApplicationType, {isClientCredentialsOnlyGrantSet} from '../../../utils/resolveApplicationType';
import ApplicationDeleteDialog from '../../ApplicationDeleteDialog';
import ClientSecretSuccessDialog from '../../ClientSecretSuccessDialog';
import CopyableField from '../../common/CopyableField';
@@ -134,13 +134,17 @@ export default function McpConnectTab({
oauth2Config?.tokenEndpointAuthMethod === TokenEndpointAuthMethods.CLIENT_SECRET_BASIC ||
oauth2Config?.tokenEndpointAuthMethod === TokenEndpointAuthMethods.CLIENT_SECRET_POST;
- // Only flow-native apps are issued a Flow Secret and can rotate it: full-stack or custom apps
- // using the embedded (non-redirect) sign-in option. Browser (public redirect) and m2m (direct
- // token) apps never hold one, regardless of OAuth config shape.
+ // Only flow-native apps are issued a Flow Secret and can rotate it: full-stack, custom, or mcp
+ // apps using the embedded (non-redirect) sign-in option. Browser (public redirect) and m2m (direct
+ // token, including client_credentials-only mcp configs) apps never hold one, regardless of OAuth
+ // config shape.
const grantTypes = oauth2Config?.grantTypes ?? [];
const isFlowNativeClient =
- (resolvedType === 'fullstack' || resolvedType === 'custom') &&
- (!oauth2Config || (!oauth2Config.publicClient && !grantTypes.includes('authorization_code')));
+ (resolvedType === 'fullstack' || resolvedType === 'custom' || resolvedType === 'mcp') &&
+ (!oauth2Config ||
+ (!oauth2Config.publicClient &&
+ !grantTypes.includes('authorization_code') &&
+ !isClientCredentialsOnlyGrantSet(grantTypes)));
const handleRegenerateClick = useCallback((): void => {
setRegenerateDialogOpen(true);
diff --git a/frontend/apps/console/src/features/applications/components/edit-application/mcp/__tests__/McpConnectTab.test.tsx b/frontend/apps/console/src/features/applications/components/edit-application/mcp/__tests__/McpConnectTab.test.tsx
index bad5d26f72..7dcdaae184 100644
--- a/frontend/apps/console/src/features/applications/components/edit-application/mcp/__tests__/McpConnectTab.test.tsx
+++ b/frontend/apps/console/src/features/applications/components/edit-application/mcp/__tests__/McpConnectTab.test.tsx
@@ -377,6 +377,19 @@ describe('McpConnectTab', () => {
expect(screen.queryByTestId('danger-zone-regenerate-flow-secret-button')).not.toBeInTheDocument();
});
+ it('does not show regenerate Flow Secret for an explicit mcp type with a client_credentials-only config', () => {
+ render(
+ ,
+ );
+
+ expect(screen.queryByTestId('danger-zone-regenerate-flow-secret-button')).not.toBeInTheDocument();
+ });
+
it('shows and wires regenerate Flow Secret for a flow-native oauth2Config', async () => {
const user = userEvent.setup();
const flowNativeOAuth2Config: OAuth2Config = {
diff --git a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json
index 26b8145d20..726fc1dbe4 100644
--- a/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json
+++ b/frontend/apps/console/src/features/applications/data/application-templates/technology-based/mcp-client.json
@@ -1,6 +1,6 @@
{
"id": "mcp-client",
- "type": "browser",
+ "type": "mcp",
"displayName": "MCP Client",
"description": "AI application that connects to MCP servers using the Model Context Protocol",
"creationFlow": {
diff --git a/frontend/apps/console/src/features/applications/models/application.ts b/frontend/apps/console/src/features/applications/models/application.ts
index 01c1e586d1..ac7d47d87f 100644
--- a/frontend/apps/console/src/features/applications/models/application.ts
+++ b/frontend/apps/console/src/features/applications/models/application.ts
@@ -28,11 +28,12 @@ import type {AssertionConfig} from './token';
* - `fullstack`: confidential client with a server component.
* - `mobile`: native/public client, uses platform attestation.
* - `m2m`: machine-to-machine, `client_credentials` only.
+ * - `mcp`: MCP client, may be public or confidential; derives behavior from its OAuth profile.
* - `custom`: no enforced constraints (escape hatch / legacy).
*
* @public
*/
-export type ApplicationType = 'browser' | 'fullstack' | 'mobile' | 'm2m' | 'custom';
+export type ApplicationType = 'browser' | 'fullstack' | 'mobile' | 'm2m' | 'mcp' | 'custom';
/**
* Application Response Model (Basic)
diff --git a/frontend/apps/console/src/features/applications/pages/__tests__/ApplicationCreatePage.test.tsx b/frontend/apps/console/src/features/applications/pages/__tests__/ApplicationCreatePage.test.tsx
index 1e5904fc70..b94ce0032e 100644
--- a/frontend/apps/console/src/features/applications/pages/__tests__/ApplicationCreatePage.test.tsx
+++ b/frontend/apps/console/src/features/applications/pages/__tests__/ApplicationCreatePage.test.tsx
@@ -479,6 +479,7 @@ function TemplateSeeder(): JSX.Element {
onClick={() =>
seed(null, null, {
id: 'mcp-client',
+ type: 'mcp',
creationFlow: {steps: ['NAME', 'ORGANIZATION_UNIT', 'CLIENT_TYPE', 'COMPLETE']},
defaults: {
inboundAuthConfig: [
@@ -2162,8 +2163,8 @@ describe('ApplicationCreatePage', () => {
isRegistrationFlowEnabled?: boolean;
};
expect(requestBody.template).toBe('mcp-client');
- // The user-delegated MCP client resolves to the browser type.
- expect(requestBody.type).toBe('browser');
+ // The user-delegated MCP client resolves to the mcp type.
+ expect(requestBody.type).toBe('mcp');
const oauth2Config = requestBody.inboundAuthConfig?.[0];
expect(oauth2Config?.type).toBe('oauth2');
diff --git a/frontend/apps/console/src/features/applications/utils/__tests__/resolveApplicationType.test.ts b/frontend/apps/console/src/features/applications/utils/__tests__/resolveApplicationType.test.ts
index cccbdec774..a679f456e5 100644
--- a/frontend/apps/console/src/features/applications/utils/__tests__/resolveApplicationType.test.ts
+++ b/frontend/apps/console/src/features/applications/utils/__tests__/resolveApplicationType.test.ts
@@ -35,6 +35,7 @@ describe('resolveApplicationType', () => {
expect(resolveApplicationType('m2m')).toBe('m2m');
expect(resolveApplicationType('mobile')).toBe('mobile');
expect(resolveApplicationType('browser', makeConfig({grantTypes: ['client_credentials']}))).toBe('browser');
+ expect(resolveApplicationType('mcp', makeConfig({grantTypes: ['client_credentials']}))).toBe('mcp');
});
it('falls back to config inference for legacy (undefined) type', () => {
diff --git a/frontend/apps/console/src/features/applications/utils/resolveApplicationType.ts b/frontend/apps/console/src/features/applications/utils/resolveApplicationType.ts
index 48587e8a8e..6f0ae5c913 100644
--- a/frontend/apps/console/src/features/applications/utils/resolveApplicationType.ts
+++ b/frontend/apps/console/src/features/applications/utils/resolveApplicationType.ts
@@ -20,7 +20,7 @@ import type {ApplicationType} from '../models/application';
import type {OAuth2Config} from '../models/oauth';
import {OAuth2GrantTypes} from '../models/oauth';
-const KNOWN_TYPES: readonly ApplicationType[] = ['browser', 'fullstack', 'mobile', 'm2m', 'custom'];
+const KNOWN_TYPES: readonly ApplicationType[] = ['browser', 'fullstack', 'mobile', 'm2m', 'mcp', 'custom'];
/**
* Resolves the canonical application type for behavior decisions.
@@ -44,8 +44,7 @@ export default function resolveApplicationType(
}
const grantTypes = oauth2Config?.grantTypes ?? [];
- const isClientCredentialsOnly = grantTypes.length === 1 && grantTypes[0] === OAuth2GrantTypes.CLIENT_CREDENTIALS;
- if (isClientCredentialsOnly) {
+ if (isClientCredentialsOnlyGrantSet(grantTypes)) {
return 'm2m';
}
if (oauth2Config?.publicClient) {
@@ -65,3 +64,12 @@ export default function resolveApplicationType(
export function isM2MApplication(type: ApplicationType | undefined, oauth2Config?: OAuth2Config): boolean {
return resolveApplicationType(type, oauth2Config) === 'm2m';
}
+
+/**
+ * Reports whether `client_credentials` is the only configured grant type, mirroring the backend's
+ * `isM2MGrantSet`. Used to exclude m2m-shaped configurations from confidential, non-redirect
+ * (flow-native) eligibility checks for `fullstack`/`custom`/`mcp` applications.
+ */
+export function isClientCredentialsOnlyGrantSet(grantTypes: readonly string[] | undefined): boolean {
+ return (grantTypes ?? []).length === 1 && grantTypes?.[0] === OAuth2GrantTypes.CLIENT_CREDENTIALS;
+}