Compare commits

..

2 Commits

Author SHA1 Message Date
a4ccf5680d fix(web): sort new i18n keys to satisfy jsonc/sort-keys
Move the M3 forwardUserIdentity / forwardUserIdentityTip entries to their
alphabetical position (between editTitle and headerKey) so the lint rule
`jsonc/sort-keys` passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:02:33 -07:00
029b836234 feat(web): add Forward-user-identity toggle to MCP provider modal (M3)
Exposes the M2 backend flags as a switch on the MCP provider create/edit
modal so workspace admins can opt in to enterprise SSO identity-forwarding
per provider. When the toggle flips on, the modal sends
forward_user_identity=true + identity_mode="idp_token" to the console API
(which the M2 backend persists on tool_mcp_providers).

- The toggle lives between Server Identifier and the Authentication tabs;
  it overrides the static Authorization (from Auth/Headers) at invoke time.
- The form-state hook hydrates from the GET response so editing preserves
  the previous choice across sessions.
- en-US + zh-Hans strings added; other locales fall back to en-US.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:53:36 -07:00
19 changed files with 73 additions and 84 deletions

View File

@ -68,7 +68,7 @@ describe('auth refresh route', () => {
const fetchHeaders = fetchMock.mock.calls[0]?.[1]?.headers as Headers
expect(fetchHeaders.get('cookie')).toBe('refresh_token=old-refresh')
expect(response.status).toBe(303)
expect(response.headers.get('location')).toBe('/apps?category=workflow')
expect(response.headers.get('location')).toBe('http://localhost:3000/apps?category=workflow')
expect(getSetCookieHeaders(response.headers)).toEqual([
'access_token=new-access; Path=/; HttpOnly',
'refresh_token=new-refresh; Path=/; HttpOnly',
@ -85,7 +85,7 @@ describe('auth refresh route', () => {
))
expect(response.status).toBe(303)
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2Fapps')
expect(response.headers.get('location')).toBe('http://localhost:3000/signin?redirect_url=%2Fapps')
})
it('should ignore cross-origin redirect targets', async () => {
@ -99,19 +99,6 @@ describe('auth refresh route', () => {
))
expect(response.status).toBe(303)
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2Fapps')
})
it('should not leak internal request origin when redirecting to signin', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 })))
const { GET } = await import('../route')
const response = await GET(createRequest(
'http://internal-service:3000/auth/refresh?redirect_url=%2F',
'refresh_token=expired',
))
expect(response.status).toBe(303)
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2F')
expect(response.headers.get('location')).toBe('http://localhost:3000/signin?redirect_url=%2Fapps')
})
})

View File

@ -18,10 +18,11 @@ const resolveAbsoluteUrlPrefix = (value: string) => {
}
}
const resolveServerConsoleApiUrl = (pathname: string) => {
const resolveServerConsoleApiUrl = (pathname: string, requestUrl: URL) => {
const requestPath = withoutLeadingSlash(pathname)
const apiPrefix = SERVER_CONSOLE_API_PREFIX
|| resolveAbsoluteUrlPrefix(API_PREFIX)
|| new URL(API_PREFIX, requestUrl.origin).toString()
if (!apiPrefix)
return null
@ -64,10 +65,10 @@ const getSetCookieHeaders = (headers: Headers) => {
return setCookie ? [setCookie] : []
}
const createRedirectResponse = (pathname: string, setCookies: string[] = []) => {
const createRedirectResponse = (request: Request, pathname: string, setCookies: string[] = []) => {
const headers = new Headers({
'Cache-Control': 'no-store',
'Location': pathname,
'Location': new URL(pathname, request.url).toString(),
})
for (const cookie of setCookies)
@ -79,16 +80,17 @@ const createRedirectResponse = (pathname: string, setCookies: string[] = []) =>
})
}
const createSigninRedirectResponse = (redirectPath: string) =>
createRedirectResponse(`${basePath}/signin?redirect_url=${encodeURIComponent(redirectPath)}`)
const createSigninRedirectResponse = (request: Request, redirectPath: string) =>
createRedirectResponse(request, `${basePath}/signin?redirect_url=${encodeURIComponent(redirectPath)}`)
export async function GET(request: Request) {
const requestUrl = new URL(request.url)
const redirectPath = resolveSafeRedirectPath(request)
const refreshUrl = resolveServerConsoleApiUrl(REFRESH_TOKEN_PATH)
const refreshUrl = resolveServerConsoleApiUrl(REFRESH_TOKEN_PATH, requestUrl)
const cookie = request.headers.get('cookie')
if (!refreshUrl || !cookie)
return createSigninRedirectResponse(redirectPath)
return createSigninRedirectResponse(request, redirectPath)
try {
const response = await fetch(refreshUrl, {
@ -101,11 +103,11 @@ export async function GET(request: Request) {
})
if (!response.ok)
return createSigninRedirectResponse(redirectPath)
return createSigninRedirectResponse(request, redirectPath)
return createRedirectResponse(redirectPath, getSetCookieHeaders(response.headers))
return createRedirectResponse(request, redirectPath, getSetCookieHeaders(response.headers))
}
catch {
return createSigninRedirectResponse(redirectPath)
return createSigninRedirectResponse(request, redirectPath)
}
}

View File

@ -54,6 +54,7 @@ type MCPModalFormState = {
isDynamicRegistration: boolean
clientID: string
credentials: string
forwardUserIdentity: boolean
}
type MCPModalFormActions = {
setUrl: (url: string) => void
@ -68,6 +69,7 @@ type MCPModalFormActions = {
setIsDynamicRegistration: (value: boolean) => void
setClientID: (id: string) => void
setCredentials: (credentials: string) => void
setForwardUserIdentity: (value: boolean) => void
handleUrlBlur: (url: string) => Promise<void>
resetIcon: () => void
}
@ -100,6 +102,11 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
const [isDynamicRegistration, setIsDynamicRegistration] = useState(() => isCreate ? true : (data?.is_dynamic_registration ?? true))
const [clientID, setClientID] = useState(() => data?.authentication?.client_id || '')
const [credentials, setCredentials] = useState(() => data?.authentication?.client_secret || '')
// M3 — user-identity forwarding. Identity mode is implied by the toggle:
// off → "off", on → "idp_token" (only mode currently supported).
const [forwardUserIdentity, setForwardUserIdentity] = useState(
() => Boolean(data?.forward_user_identity),
)
const handleUrlBlur = useCallback(async (urlValue: string) => {
if (data)
return
@ -163,6 +170,7 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
isDynamicRegistration,
clientID,
credentials,
forwardUserIdentity,
} satisfies MCPModalFormState,
// Actions
actions: {
@ -178,6 +186,7 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
setIsDynamicRegistration,
setClientID,
setCredentials,
setForwardUserIdentity,
handleUrlBlur,
resetIcon,
} satisfies MCPModalFormActions,

View File

@ -5,6 +5,7 @@ import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { AppIconType } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine, RiEditLine } from '@remixicon/react'
import { useHover } from 'ahooks'
@ -39,6 +40,8 @@ type MCPModalConfirmPayload = {
timeout: number
sse_read_timeout: number
}
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
}
type DuplicateAppModalProps = {
@ -110,6 +113,8 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
timeout: state.timeout || 30,
sse_read_timeout: state.sseReadTimeout || 300,
},
forward_user_identity: state.forwardUserIdentity,
identity_mode: state.forwardUserIdentity ? 'idp_token' : 'off',
})
if (isCreate)
onHide()
@ -207,6 +212,23 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
)}
</div>
{/* Forward user identity (M3 — enterprise SSO identity-forwarding) */}
<div>
<div className="mb-1 flex h-6 items-center">
<Switch
className="mr-2"
checked={state.forwardUserIdentity}
onCheckedChange={actions.setForwardUserIdentity}
/>
<span 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 */}
<TabSlider
className="w-full"

View File

@ -78,6 +78,9 @@ export type Collection = {
timeout?: number
sse_read_timeout?: number
}
// M3 — user-identity forwarding (MCP)
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
// Workflow
workflow_app_id?: string
}

View File

@ -196,7 +196,7 @@ export default function DevicePage() {
<h1 className="text-xl font-semibold text-text-primary">You&apos;re signed in</h1>
<p className="text-sm text-text-secondary">Return to your terminal to continue.</p>
<Divider className="my-3" />
<Button variant="ghost" className="w-full" onClick={() => router.push('/')}>
<Button variant="ghost" className="w-full" onClick={() => router.push('/apps')}>
Go to Dify console
</Button>
</div>

View File

@ -59,7 +59,7 @@ describe('InstallForm', () => {
expect(mockSetup).not.toHaveBeenCalled()
})
it('should submit and redirect to the console root on successful login', async () => {
it('should submit and redirect to apps on successful login', async () => {
mockSetup.mockResolvedValue({ result: 'success' } as any)
mockLogin.mockResolvedValue({ result: 'success', data: { access_token: 'token' } } as any)
@ -96,7 +96,7 @@ describe('InstallForm', () => {
})
await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith('/')
expect(mockReplace).toHaveBeenCalledWith('/apps')
})
})

View File

@ -4,7 +4,6 @@ import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useStore } from '@tanstack/react-form'
import { useQueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -18,7 +17,6 @@ import { LICENSE_LINK } from '@/constants/link'
import useDocumentTitle from '@/hooks/use-document-title'
import Link from '@/next/link'
import { useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
import { encryptPassword as encodePassword } from '@/utils/encryption'
import Loading from '../components/base/loading'
@ -40,7 +38,6 @@ const InstallForm = () => {
useDocumentTitle('')
const { t, i18n } = useTranslation()
const router = useRouter()
const queryClient = useQueryClient()
const [showPassword, setShowPassword] = React.useState(false)
const [loading, setLoading] = React.useState(true)
@ -71,10 +68,9 @@ const InstallForm = () => {
},
})
// Store tokens and redirect if login successful
// Store tokens and redirect to apps if login successful
if (loginRes.result === 'success') {
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
router.replace('/')
router.replace('/apps')
}
else {
// Fallback to signin page if auto-login fails

View File

@ -3,7 +3,6 @@ import type { FormEvent } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useQueryClient } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
@ -12,7 +11,6 @@ import Countdown from '@/app/components/signin/countdown'
import { useLocale } from '@/context/i18n'
import { useRouter, useSearchParams } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
import { encryptVerificationCode } from '@/utils/encryption'
import { getBrowserTimezone } from '@/utils/timezone'
@ -21,7 +19,6 @@ import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
export default function CheckCode() {
const { t, i18n } = useTranslation()
const router = useRouter()
const queryClient = useQueryClient()
const searchParams = useSearchParams()
const email = decodeURIComponent(searchParams.get('email') as string)
const token = decodeURIComponent(searchParams.get('token') as string)
@ -61,9 +58,8 @@ export default function CheckCode() {
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}
else {
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/')
router.replace(redirectUrl || '/apps')
}
}
}

View File

@ -1,7 +1,6 @@
import type { ResponseError } from '@/service/fetch'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueryClient } from '@tanstack/react-query'
import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -11,7 +10,6 @@ import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { login } from '@/service/common'
import { setWebAppAccessToken } from '@/service/webapp-auth'
import { encryptPassword } from '@/utils/encryption'
@ -27,7 +25,6 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
const { t } = useTranslation()
const locale = useLocale()
const router = useRouter()
const queryClient = useQueryClient()
const searchParams = useSearchParams()
const [showPassword, setShowPassword] = useState(false)
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
@ -78,9 +75,8 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}
else {
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/')
router.replace(redirectUrl || '/apps')
}
}
else {

View File

@ -12,9 +12,6 @@ vi.mock('@tanstack/react-query', async () => {
const actual = await vi.importActual<typeof import('@tanstack/react-query')>('@tanstack/react-query')
return {
...actual,
useQueryClient: vi.fn(() => ({
resetQueries: vi.fn(),
})),
useSuspenseQuery: vi.fn(() => ({
data: {
branding: {

View File

@ -4,7 +4,7 @@ import { Button } from '@langgenius/dify-ui/button'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { toast } from '@langgenius/dify-ui/toast'
import { RiAccountCircleLine } from '@remixicon/react'
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { useSuspenseQuery } from '@tanstack/react-query'
import { noop } from 'es-toolkit/function'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -16,7 +16,6 @@ import { i18n, setLocaleOnClient } from '@/i18n-config'
import { languages } from '@/i18n-config/language'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { activateMember } from '@/service/common'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useInvitationCheck } from '@/service/use-common'
@ -56,7 +55,6 @@ export default function InviteSettingsPage() {
const { t } = useTranslation()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const router = useRouter()
const queryClient = useQueryClient()
const searchParams = useSearchParams()
const token = decodeURIComponent(searchParams.get('invite_token') as string)
const locale = useLocale()
@ -104,15 +102,14 @@ export default function InviteSettingsPage() {
if (res.result === 'success') {
// Tokens are now stored in cookies by the backend
await setLocaleOnClient(language!, false)
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/')
router.replace(redirectUrl || '/apps')
}
}
catch {
recheck()
}
}, [language, name, queryClient, recheck, searchParams, timezone, token, router, t])
}, [language, name, recheck, timezone, token, router, t])
if (!checkRes)
return <Loading />

View File

@ -50,7 +50,7 @@ const NormalForm = () => {
if (isLoggedIn) {
setIsRedirecting(true)
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/')
router.replace(redirectUrl || '/apps')
return
}
@ -75,7 +75,7 @@ const NormalForm = () => {
setAllMethodsAreDisabled(true)
}
finally { setInitCheckLoading(false) }
}, [isLoggedIn, message, router, searchParams, invite_token, isInviteLink, systemFeatures])
}, [isLoggedIn, message, router, invite_token, isInviteLink, systemFeatures])
useEffect(() => {
init()
}, [init])

View File

@ -4,14 +4,12 @@ import { Button } from '@langgenius/dify-ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueryClient } from '@tanstack/react-query'
import { useReducer } from 'react'
import { useTranslation } from 'react-i18next'
import { LICENSE_LINK } from '@/constants/link'
import { languages } from '@/i18n-config/language'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { useOneMoreStep } from '@/service/use-common'
import { timezones } from '@/utils/timezone'
import Input from '../components/base/input'
@ -68,7 +66,6 @@ const hasStatus = (error: unknown): error is { status: number } => {
const OneMoreStep = () => {
const { t } = useTranslation()
const router = useRouter()
const queryClient = useQueryClient()
const searchParams = useSearchParams()
const [state, dispatch] = useReducer(reducer, {
@ -101,8 +98,7 @@ const OneMoreStep = () => {
interface_language: state.interface_language,
timezone: state.timezone,
})
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
router.replace('/')
router.push('/apps')
}
catch (error: unknown) {
if (hasStatus(error) && error.status === 400)

View File

@ -1,6 +1,4 @@
import type { ReactElement } from 'react'
import type { MockedFunction } from 'vitest'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useLocale } from '@/context/i18n'
@ -47,20 +45,6 @@ const mockUseRouter = useRouter as unknown as MockedFunction<typeof useRouter>
const mockUseMailRegister = useMailRegister as unknown as MockedFunction<typeof useMailRegister>
const mockGetBrowserTimezone = getBrowserTimezone as unknown as MockedFunction<typeof getBrowserTimezone>
const renderWithQueryClient = (ui: ReactElement) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>,
)
}
describe('Signup Set Password Page', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -77,7 +61,7 @@ describe('Signup Set Password Page', () => {
describe('Registration payload', () => {
it('should submit locale and browser timezone when setting password', async () => {
renderWithQueryClient(<ChangePasswordForm />)
render(<ChangePasswordForm />)
fireEvent.change(screen.getByLabelText('common.account.newPassword'), {
target: { value: 'ValidPass123!' },

View File

@ -3,7 +3,6 @@ import type { MailRegisterResponse } from '@/service/use-common'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueryClient } from '@tanstack/react-query'
import Cookies from 'js-cookie'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -12,7 +11,6 @@ import Input from '@/app/components/base/input'
import { validPassword } from '@/config'
import { useLocale } from '@/context/i18n'
import { useRouter, useSearchParams } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { useMailRegister } from '@/service/use-common'
import { rememberCreateAppExternalAttribution } from '@/utils/create-app-tracking'
import { sendGAEvent } from '@/utils/gtag'
@ -34,7 +32,6 @@ const parseUtmInfo = () => {
const ChangePasswordForm = () => {
const { t } = useTranslation()
const router = useRouter()
const queryClient = useQueryClient()
const searchParams = useSearchParams()
const token = decodeURIComponent(searchParams.get('token') || '')
const locale = useLocale()
@ -90,14 +87,13 @@ const ChangePasswordForm = () => {
Cookies.remove('utm_info') // Clean up: remove utm_info cookie
toast.success(t('api.actionSuccess', { ns: 'common' }))
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
router.replace('/')
router.replace('/apps')
}
}
catch (error) {
console.error(error)
}
}, [password, token, valid, confirmPassword, register, locale, queryClient, router, t])
}, [password, token, valid, confirmPassword, register, locale])
return (
<div className={

View File

@ -120,6 +120,8 @@
"mcp.modal.configurations": "Configurations",
"mcp.modal.confirm": "Add & Authorize",
"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.headerKeyPlaceholder": "e.g., Authorization",
"mcp.modal.headerValue": "Header Value",

View File

@ -120,6 +120,8 @@
"mcp.modal.configurations": "配置",
"mcp.modal.confirm": "添加并授权",
"mcp.modal.editTitle": "修改 MCP 服务 (HTTP)",
"mcp.modal.forwardUserIdentity": "转发用户身份",
"mcp.modal.forwardUserIdentityTip": "将调用用户的已验证 SSO 身份作为 Authorization Bearer token 转发到该 MCP 服务器。需要 Dify Enterprise SSO。",
"mcp.modal.headerKey": "请求头名称",
"mcp.modal.headerKeyPlaceholder": "例如Authorization",
"mcp.modal.headerValue": "请求头值",

View File

@ -106,6 +106,8 @@ export const useCreateMCP = () => {
timeout?: number
sse_read_timeout?: number
headers?: Record<string, string>
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
}) => {
return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', {
body: {
@ -133,6 +135,8 @@ export const useUpdateMCP = ({
timeout?: number
sse_read_timeout?: number
headers?: Record<string, string>
forward_user_identity?: boolean
identity_mode?: 'off' | 'idp_token'
}) => {
return put('workspaces/current/tool-provider/mcp', {
body: {