mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
feat(web): add Forward-user-identity toggle to MCP provider modal (#36840)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
23cd129802
commit
d16a012575
@ -30,6 +30,21 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Default: SSO off entirely. Tests that need the toggle visible flip
|
||||||
|
// `sso_enforced_for_signin = true` AND set the protocol to 'oidc' or
|
||||||
|
// 'oauth2'. Tests for the SAML gate set protocol = 'saml' to assert the
|
||||||
|
// toggle stays hidden even when sso_enforced_for_signin is true.
|
||||||
|
const mockSystemFeatures = vi.hoisted(() => ({
|
||||||
|
sso_enforced_for_signin: false,
|
||||||
|
sso_enforced_for_signin_protocol: '' as 'oidc' | 'oauth2' | 'saml' | '',
|
||||||
|
}))
|
||||||
|
vi.mock('@/features/system-features/client', () => ({
|
||||||
|
systemFeaturesQueryOptions: () => ({
|
||||||
|
queryKey: ['mock-system-features'],
|
||||||
|
queryFn: async () => mockSystemFeatures,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
describe('MCPModal', () => {
|
describe('MCPModal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
@ -43,6 +58,9 @@ describe('MCPModal', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
// useSuspenseQuery(systemFeaturesQueryOptions) reads from this key —
|
||||||
|
// pre-populate so the modal renders synchronously instead of suspending.
|
||||||
|
queryClient.setQueryData(['mock-system-features'], mockSystemFeatures)
|
||||||
return ({ children }: { children: ReactNode }) =>
|
return ({ children }: { children: ReactNode }) =>
|
||||||
React.createElement(QueryClientProvider, { client: queryClient }, children)
|
React.createElement(QueryClientProvider, { client: queryClient }, children)
|
||||||
}
|
}
|
||||||
@ -719,4 +737,132 @@ describe('MCPModal', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// M3 — Forward-user-identity toggle (PR #36840).
|
||||||
|
describe('Forward-user-identity toggle', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin = false
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin_protocol = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper: turn SSO on with a refresh-capable protocol so the toggle is
|
||||||
|
// visible. Use this for any test that needs the field rendered.
|
||||||
|
const enableRefreshCapableSSO = () => {
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin = true
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin_protocol = 'oidc'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillRequiredFields = () => {
|
||||||
|
fireEvent.change(
|
||||||
|
screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder'),
|
||||||
|
{ target: { value: 'https://example.com/mcp' } },
|
||||||
|
)
|
||||||
|
fireEvent.change(
|
||||||
|
screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder'),
|
||||||
|
{ target: { value: 'srv' } },
|
||||||
|
)
|
||||||
|
fireEvent.change(
|
||||||
|
screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder'),
|
||||||
|
{ target: { value: 'srv-id' } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('does not render the toggle when SSO is not configured', () => {
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin = false
|
||||||
|
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||||
|
expect(screen.queryByText('tools.mcp.modal.forwardUserIdentity')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the toggle and helper tip when SSO is configured', () => {
|
||||||
|
enableRefreshCapableSSO()
|
||||||
|
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||||
|
expect(screen.getByText('tools.mcp.modal.forwardUserIdentity')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('tools.mcp.modal.forwardUserIdentityTip')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render the toggle when SSO protocol is SAML (no refresh model)', () => {
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin = true
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin_protocol = 'saml'
|
||||||
|
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||||
|
expect(screen.queryByText('tools.mcp.modal.forwardUserIdentity')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the toggle when SSO protocol is OAuth2', () => {
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin = true
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin_protocol = 'oauth2'
|
||||||
|
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||||
|
expect(screen.getByText('tools.mcp.modal.forwardUserIdentity')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits identity_mode="off" by default (toggle off)', async () => {
|
||||||
|
enableRefreshCapableSSO()
|
||||||
|
const onConfirm = vi.fn()
|
||||||
|
render(
|
||||||
|
<MCPModal {...defaultProps} onConfirm={onConfirm} />,
|
||||||
|
{ wrapper: createWrapper() },
|
||||||
|
)
|
||||||
|
|
||||||
|
fillRequiredFields()
|
||||||
|
fireEvent.click(screen.getByText('tools.mcp.modal.confirm'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onConfirm).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
identity_mode: 'off',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits identity_mode="idp_token" when toggle is flipped on', async () => {
|
||||||
|
enableRefreshCapableSSO()
|
||||||
|
const onConfirm = vi.fn()
|
||||||
|
render(
|
||||||
|
<MCPModal {...defaultProps} onConfirm={onConfirm} />,
|
||||||
|
{ wrapper: createWrapper() },
|
||||||
|
)
|
||||||
|
|
||||||
|
fillRequiredFields()
|
||||||
|
const fwdSwitch = screen.getByRole('switch', {
|
||||||
|
name: 'tools.mcp.modal.forwardUserIdentity',
|
||||||
|
})
|
||||||
|
fireEvent.click(fwdSwitch)
|
||||||
|
fireEvent.click(screen.getByText('tools.mcp.modal.confirm'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onConfirm).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
identity_mode: 'idp_token',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps to identity_mode="off" when SSO is unavailable, even if existing data had it on', async () => {
|
||||||
|
mockSystemFeatures.sso_enforced_for_signin = false
|
||||||
|
const onConfirm = vi.fn()
|
||||||
|
const mockData = {
|
||||||
|
id: 'existing-1',
|
||||||
|
name: 'srv',
|
||||||
|
server_url: 'https://example.com/mcp',
|
||||||
|
server_identifier: 'srv-id',
|
||||||
|
icon: { content: '🔗', background: '#6366F1' },
|
||||||
|
identity_mode: 'idp_token',
|
||||||
|
} as unknown as ToolWithProvider
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MCPModal {...defaultProps} data={mockData} onConfirm={onConfirm} />,
|
||||||
|
{ wrapper: createWrapper() },
|
||||||
|
)
|
||||||
|
fireEvent.click(screen.getByText('tools.mcp.modal.save'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onConfirm).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
identity_mode: 'off',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -497,4 +497,51 @@ describe('useMCPModalForm', () => {
|
|||||||
expect(((result.current.state.appIcon) as AppIconImageSelection).url).toBe('https://example.com/icon.png')
|
expect(((result.current.state.appIcon) as AppIconImageSelection).url).toBe('https://example.com/icon.png')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// M3 — Forward-user-identity toggle (PR #36840). The hook stores a bool,
|
||||||
|
// hydrates it from data.identity_mode (true iff non-"off"), and exposes a
|
||||||
|
// setter.
|
||||||
|
describe('Forward-user-identity toggle', () => {
|
||||||
|
it('defaults to false in create mode', () => {
|
||||||
|
const { result } = renderHook(() => useMCPModalForm())
|
||||||
|
expect(result.current.state.forwardUserIdentity).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hydrates as true when data.identity_mode is "idp_token"', () => {
|
||||||
|
const mockData = {
|
||||||
|
id: 'existing-1',
|
||||||
|
icon: { content: '🔗', background: '#6366F1' },
|
||||||
|
identity_mode: 'idp_token',
|
||||||
|
} as unknown as ToolWithProvider
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useMCPModalForm(mockData))
|
||||||
|
expect(result.current.state.forwardUserIdentity).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hydrates as false when data.identity_mode is missing or "off"', () => {
|
||||||
|
const mockData = {
|
||||||
|
id: 'existing-2',
|
||||||
|
icon: { content: '🔗', background: '#6366F1' },
|
||||||
|
// identity_mode intentionally omitted
|
||||||
|
} as unknown as ToolWithProvider
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useMCPModalForm(mockData))
|
||||||
|
expect(result.current.state.forwardUserIdentity).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates state via setForwardUserIdentity', () => {
|
||||||
|
const { result } = renderHook(() => useMCPModalForm())
|
||||||
|
expect(result.current.state.forwardUserIdentity).toBe(false)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.actions.setForwardUserIdentity(true)
|
||||||
|
})
|
||||||
|
expect(result.current.state.forwardUserIdentity).toBe(true)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.actions.setForwardUserIdentity(false)
|
||||||
|
})
|
||||||
|
expect(result.current.state.forwardUserIdentity).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -54,6 +54,7 @@ type MCPModalFormState = {
|
|||||||
isDynamicRegistration: boolean
|
isDynamicRegistration: boolean
|
||||||
clientID: string
|
clientID: string
|
||||||
credentials: string
|
credentials: string
|
||||||
|
forwardUserIdentity: boolean
|
||||||
}
|
}
|
||||||
type MCPModalFormActions = {
|
type MCPModalFormActions = {
|
||||||
setUrl: (url: string) => void
|
setUrl: (url: string) => void
|
||||||
@ -68,6 +69,7 @@ type MCPModalFormActions = {
|
|||||||
setIsDynamicRegistration: (value: boolean) => void
|
setIsDynamicRegistration: (value: boolean) => void
|
||||||
setClientID: (id: string) => void
|
setClientID: (id: string) => void
|
||||||
setCredentials: (credentials: string) => void
|
setCredentials: (credentials: string) => void
|
||||||
|
setForwardUserIdentity: (value: boolean) => void
|
||||||
handleUrlBlur: (url: string) => Promise<void>
|
handleUrlBlur: (url: string) => Promise<void>
|
||||||
resetIcon: () => void
|
resetIcon: () => void
|
||||||
}
|
}
|
||||||
@ -100,6 +102,11 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
|
|||||||
const [isDynamicRegistration, setIsDynamicRegistration] = useState(() => isCreate ? true : (data?.is_dynamic_registration ?? true))
|
const [isDynamicRegistration, setIsDynamicRegistration] = useState(() => isCreate ? true : (data?.is_dynamic_registration ?? true))
|
||||||
const [clientID, setClientID] = useState(() => data?.authentication?.client_id || '')
|
const [clientID, setClientID] = useState(() => data?.authentication?.client_id || '')
|
||||||
const [credentials, setCredentials] = useState(() => data?.authentication?.client_secret || '')
|
const [credentials, setCredentials] = useState(() => data?.authentication?.client_secret || '')
|
||||||
|
// M3 — user-identity forwarding. The UI toggle is true iff the persisted
|
||||||
|
// identity_mode is anything other than "off" — currently just "idp_token".
|
||||||
|
const [forwardUserIdentity, setForwardUserIdentity] = useState(
|
||||||
|
() => (data?.identity_mode ?? 'off') !== 'off',
|
||||||
|
)
|
||||||
const handleUrlBlur = useCallback(async (urlValue: string) => {
|
const handleUrlBlur = useCallback(async (urlValue: string) => {
|
||||||
if (data)
|
if (data)
|
||||||
return
|
return
|
||||||
@ -163,6 +170,7 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
|
|||||||
isDynamicRegistration,
|
isDynamicRegistration,
|
||||||
clientID,
|
clientID,
|
||||||
credentials,
|
credentials,
|
||||||
|
forwardUserIdentity,
|
||||||
} satisfies MCPModalFormState,
|
} satisfies MCPModalFormState,
|
||||||
// Actions
|
// Actions
|
||||||
actions: {
|
actions: {
|
||||||
@ -178,6 +186,7 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
|
|||||||
setIsDynamicRegistration,
|
setIsDynamicRegistration,
|
||||||
setClientID,
|
setClientID,
|
||||||
setCredentials,
|
setCredentials,
|
||||||
|
setForwardUserIdentity,
|
||||||
handleUrlBlur,
|
handleUrlBlur,
|
||||||
resetIcon,
|
resetIcon,
|
||||||
} satisfies MCPModalFormActions,
|
} satisfies MCPModalFormActions,
|
||||||
|
|||||||
@ -5,8 +5,10 @@ import type { ToolWithProvider } from '@/app/components/workflow/types'
|
|||||||
import type { AppIconType } from '@/types/app'
|
import type { AppIconType } from '@/types/app'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||||
|
import { Switch } from '@langgenius/dify-ui/switch'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { RiCloseLine, RiEditLine } from '@remixicon/react'
|
import { RiCloseLine, RiEditLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useHover } from 'ahooks'
|
import { useHover } from 'ahooks'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
@ -15,12 +17,20 @@ import { Mcp } from '@/app/components/base/icons/src/vender/other'
|
|||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import TabSlider from '@/app/components/base/tab-slider'
|
import TabSlider from '@/app/components/base/tab-slider'
|
||||||
import { MCPAuthMethod } from '@/app/components/tools/types'
|
import { MCPAuthMethod } from '@/app/components/tools/types'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { shouldUseMcpIconForAppIcon } from '@/utils/mcp'
|
import { shouldUseMcpIconForAppIcon } from '@/utils/mcp'
|
||||||
import { isValidServerID, isValidUrl, useMCPModalForm } from './hooks/use-mcp-modal-form'
|
import { isValidServerID, isValidUrl, useMCPModalForm } from './hooks/use-mcp-modal-form'
|
||||||
import AuthenticationSection from './sections/authentication-section'
|
import AuthenticationSection from './sections/authentication-section'
|
||||||
import ConfigurationsSection from './sections/configurations-section'
|
import ConfigurationsSection from './sections/configurations-section'
|
||||||
import HeadersSection from './sections/headers-section'
|
import HeadersSection from './sections/headers-section'
|
||||||
|
|
||||||
|
// SSO protocols whose token-endpoint flow supports refresh-token issuance and
|
||||||
|
// therefore can back MCP per-user identity forwarding. SAML cannot — it has
|
||||||
|
// no refresh model and no token endpoint, so the enterprise side returns the
|
||||||
|
// disabled stub for it.
|
||||||
|
const MCP_FORWARDING_CAPABLE_PROTOCOLS = ['oidc', 'oauth2'] as const
|
||||||
|
type MCPForwardingCapableProtocol = typeof MCP_FORWARDING_CAPABLE_PROTOCOLS[number]
|
||||||
|
|
||||||
type MCPModalConfirmPayload = {
|
type MCPModalConfirmPayload = {
|
||||||
name: string
|
name: string
|
||||||
server_url: string
|
server_url: string
|
||||||
@ -39,6 +49,7 @@ type MCPModalConfirmPayload = {
|
|||||||
timeout: number
|
timeout: number
|
||||||
sse_read_timeout: number
|
sse_read_timeout: number
|
||||||
}
|
}
|
||||||
|
identity_mode?: 'off' | 'idp_token'
|
||||||
}
|
}
|
||||||
|
|
||||||
type DuplicateAppModalProps = {
|
type DuplicateAppModalProps = {
|
||||||
@ -70,6 +81,13 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
|
|||||||
actions,
|
actions,
|
||||||
} = useMCPModalForm(data)
|
} = useMCPModalForm(data)
|
||||||
|
|
||||||
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
// SAML has no refresh_token model, so the enterprise side can't mint
|
||||||
|
// per-call MCP tokens. Only OIDC and OAuth2 can — gate the toggle on
|
||||||
|
// both "SSO enforced" AND "protocol is refresh-capable".
|
||||||
|
const ssoProtocol = systemFeatures.sso_enforced_for_signin_protocol as MCPForwardingCapableProtocol
|
||||||
|
const isForwardIdentitySupported = systemFeatures.sso_enforced_for_signin && MCP_FORWARDING_CAPABLE_PROTOCOLS.includes(ssoProtocol)
|
||||||
|
|
||||||
const isHovering = useHover(appIconRef)
|
const isHovering = useHover(appIconRef)
|
||||||
|
|
||||||
const authMethods = [
|
const authMethods = [
|
||||||
@ -110,6 +128,9 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
|
|||||||
timeout: state.timeout || 30,
|
timeout: state.timeout || 30,
|
||||||
sse_read_timeout: state.sseReadTimeout || 300,
|
sse_read_timeout: state.sseReadTimeout || 300,
|
||||||
},
|
},
|
||||||
|
// Edit-mode data may carry idp_token; clamp to off when SSO is no
|
||||||
|
// longer available so a stale row can't keep forwarding configured.
|
||||||
|
identity_mode: state.forwardUserIdentity && isForwardIdentitySupported ? 'idp_token' : 'off',
|
||||||
})
|
})
|
||||||
if (isCreate)
|
if (isCreate)
|
||||||
onHide()
|
onHide()
|
||||||
@ -207,6 +228,28 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isForwardIdentitySupported && (
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 flex h-6 items-center">
|
||||||
|
<Switch
|
||||||
|
className="mr-2"
|
||||||
|
checked={state.forwardUserIdentity}
|
||||||
|
onCheckedChange={actions.setForwardUserIdentity}
|
||||||
|
aria-labelledby="mcp-forward-user-identity-label"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
id="mcp-forward-user-identity-label"
|
||||||
|
className="system-sm-medium text-text-secondary"
|
||||||
|
>
|
||||||
|
{t('mcp.modal.forwardUserIdentity', { ns: 'tools' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="body-xs-regular text-text-tertiary">
|
||||||
|
{t('mcp.modal.forwardUserIdentityTip', { ns: 'tools' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Auth Method Tabs */}
|
{/* Auth Method Tabs */}
|
||||||
<TabSlider
|
<TabSlider
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
@ -78,6 +78,11 @@ export type Collection = {
|
|||||||
timeout?: number
|
timeout?: number
|
||||||
sse_read_timeout?: number
|
sse_read_timeout?: number
|
||||||
}
|
}
|
||||||
|
// M3 — user-identity forwarding (MCP). Single selector now drives both
|
||||||
|
// "is forwarding on?" and "which mechanism to use?". Pre-collapse builds
|
||||||
|
// also sent a redundant `forward_user_identity` boolean; the api dropped
|
||||||
|
// it, so the field is gone here too.
|
||||||
|
identity_mode?: 'off' | 'idp_token'
|
||||||
// Workflow
|
// Workflow
|
||||||
workflow_app_id?: string
|
workflow_app_id?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,6 +120,8 @@
|
|||||||
"mcp.modal.configurations": "Configurations",
|
"mcp.modal.configurations": "Configurations",
|
||||||
"mcp.modal.confirm": "Add & Authorize",
|
"mcp.modal.confirm": "Add & Authorize",
|
||||||
"mcp.modal.editTitle": "Edit MCP Server (HTTP)",
|
"mcp.modal.editTitle": "Edit MCP Server (HTTP)",
|
||||||
|
"mcp.modal.forwardUserIdentity": "Forward user identity",
|
||||||
|
"mcp.modal.forwardUserIdentityTip": "Send the calling user's verified SSO identity to this MCP server as an Authorization Bearer token. Requires Dify Enterprise SSO.",
|
||||||
"mcp.modal.headerKey": "Header Name",
|
"mcp.modal.headerKey": "Header Name",
|
||||||
"mcp.modal.headerKeyPlaceholder": "e.g., Authorization",
|
"mcp.modal.headerKeyPlaceholder": "e.g., Authorization",
|
||||||
"mcp.modal.headerValue": "Header Value",
|
"mcp.modal.headerValue": "Header Value",
|
||||||
|
|||||||
@ -120,6 +120,8 @@
|
|||||||
"mcp.modal.configurations": "配置",
|
"mcp.modal.configurations": "配置",
|
||||||
"mcp.modal.confirm": "添加并授权",
|
"mcp.modal.confirm": "添加并授权",
|
||||||
"mcp.modal.editTitle": "修改 MCP 服务 (HTTP)",
|
"mcp.modal.editTitle": "修改 MCP 服务 (HTTP)",
|
||||||
|
"mcp.modal.forwardUserIdentity": "转发用户身份",
|
||||||
|
"mcp.modal.forwardUserIdentityTip": "将调用用户的已验证 SSO 身份作为 Authorization Bearer token 转发到该 MCP 服务器。需要 Dify Enterprise SSO。",
|
||||||
"mcp.modal.headerKey": "请求头名称",
|
"mcp.modal.headerKey": "请求头名称",
|
||||||
"mcp.modal.headerKeyPlaceholder": "例如:Authorization",
|
"mcp.modal.headerKeyPlaceholder": "例如:Authorization",
|
||||||
"mcp.modal.headerValue": "请求头值",
|
"mcp.modal.headerValue": "请求头值",
|
||||||
|
|||||||
@ -106,6 +106,7 @@ export const useCreateMCP = () => {
|
|||||||
timeout?: number
|
timeout?: number
|
||||||
sse_read_timeout?: number
|
sse_read_timeout?: number
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
|
identity_mode?: 'off' | 'idp_token'
|
||||||
}) => {
|
}) => {
|
||||||
return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', {
|
return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', {
|
||||||
body: {
|
body: {
|
||||||
@ -133,6 +134,7 @@ export const useUpdateMCP = ({
|
|||||||
timeout?: number
|
timeout?: number
|
||||||
sse_read_timeout?: number
|
sse_read_timeout?: number
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
|
identity_mode?: 'off' | 'idp_token'
|
||||||
}) => {
|
}) => {
|
||||||
return put('workspaces/current/tool-provider/mcp', {
|
return put('workspaces/current/tool-provider/mcp', {
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user