Compare commits

..

2 Commits

Author SHA1 Message Date
585d292511 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:51:36 -07:00
d3fa24d7e3 feat(api): add MCP user-identity forwarding (M2)
When an MCP provider has forward_user_identity enabled, MCPTool now asks
dify-enterprise to mint a short-lived per-user SSO id_token (via the M1
/inner/api/mcp/issue-token endpoint) and stamps it as the Authorization
Bearer on every outbound MCP request — so an MCP server can act on behalf
of the verified end user instead of seeing only "Dify is calling."

- Adds forward_user_identity (bool) + identity_mode ("off" | "idp_token")
  to tool_mcp_providers, plumbed through MCPProviderEntity, the controller,
  service-layer CRUD, and the tool/provider runtime.
- EnterpriseService.issue_mcp_token wraps the enterprise endpoint and maps
  428 → MCPNoRefreshTokenError, 401 → MCPIdentityRefreshError so workflows
  halt with a clear "please re-authenticate" instead of silently going
  anonymous.
- Identity_mode is intentionally an enum-shaped string column so future
  modes (e.g. RFC 8693 token exchange) land without UI/DB churn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:51:20 -07:00
17 changed files with 67 additions and 141 deletions

View File

@ -467,8 +467,7 @@ class AppListApi(Resource):
@login_required
@account_initialization_required
@enterprise_license_required
@with_session(write=False)
def get(self, session: Session):
def get(self):
"""Get app list"""
current_user, current_tenant_id = current_account_with_tenant()
@ -505,7 +504,7 @@ class AppListApi(Resource):
draft_trigger_app_ids: set[str] = set()
if workflow_capable_app_ids:
draft_workflows = (
session.execute(
db.session.execute(
select(Workflow).where(
Workflow.version == Workflow.VERSION_DRAFT,
Workflow.app_id.in_(workflow_capable_app_ids),

View File

@ -2,7 +2,6 @@ from collections.abc import Sequence
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from controllers.common.schema import register_enum_models, register_schema_models
from controllers.console import console_ns
@ -12,7 +11,6 @@ from controllers.console.app.error import (
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
from controllers.console.app.wraps import with_session
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from core.app.app_config.entities import ModelConfig
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
@ -21,6 +19,7 @@ from core.helper.code_executor.javascript.javascript_code_provider import Javasc
from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider
from core.llm_generator.entities import RuleCodeGeneratePayload, RuleGeneratePayload, RuleStructuredOutputPayload
from core.llm_generator.llm_generator import LLMGenerator
from extensions.ext_database import db
from graphon.model_runtime.entities.llm_entities import LLMMode
from graphon.model_runtime.errors.invoke import InvokeError
from libs.login import login_required
@ -159,8 +158,7 @@ class InstructionGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, current_tenant_id: str):
def post(self, current_tenant_id: str):
args = InstructionGeneratePayload.model_validate(console_ns.payload)
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
code_provider: type[CodeNodeProvider] | None = next(
@ -170,10 +168,10 @@ class InstructionGenerateApi(Resource):
try:
# Generate from nothing for a workflow node
if (args.current in (code_template, "")) and args.node_id != "":
app = session.get(App, args.flow_id)
app = db.session.get(App, args.flow_id)
if not app:
return {"error": f"app {args.flow_id} not found"}, 400
workflow = WorkflowService().get_draft_workflow(app_model=app, session=session)
workflow = WorkflowService().get_draft_workflow(app_model=app)
if not workflow:
return {"error": f"workflow {args.flow_id} not found"}, 400
nodes: Sequence = workflow.graph_dict["nodes"]

View File

@ -59,7 +59,9 @@ class ToolProviderApiEntity(BaseModel):
forward_user_identity: bool = Field(
default=False, description="Whether Dify forwards the calling user's SSO identity to this MCP server"
)
identity_mode: str = Field(default="off", description="Identity-forwarding mechanism: 'off' or 'idp_token'")
identity_mode: str = Field(
default="off", description="Identity-forwarding mechanism: 'off' or 'idp_token'"
)
# Workflow
workflow_app_id: str | None = Field(default=None, description="The app id of the workflow tool")

View File

@ -13761,12 +13761,10 @@ Enum class for large language model mode.
| ---- | ---- | ----------- | -------- |
| authentication | object | | No |
| configuration | object | | No |
| forward_user_identity | boolean | | No |
| headers | object | | No |
| icon | string | | Yes |
| icon_background | string | | No |
| icon_type | string | | Yes |
| identity_mode | string | *Enum:* `"idp_token"`, `"off"` | No |
| name | string | | Yes |
| server_identifier | string | | Yes |
| server_url | string | | Yes |
@ -13783,12 +13781,10 @@ Enum class for large language model mode.
| ---- | ---- | ----------- | -------- |
| authentication | object | | No |
| configuration | object | | No |
| forward_user_identity | boolean | | No |
| headers | object | | No |
| icon | string | | Yes |
| icon_background | string | | No |
| icon_type | string | | Yes |
| identity_mode | string | *Enum:* `"idp_token"`, `"off"` | No |
| name | string | | Yes |
| provider_id | string | | Yes |
| server_identifier | string | | Yes |

View File

@ -39,7 +39,6 @@ class MCPIdentityRefreshError(MCPTokenError):
def __init__(self, description: str = ""):
super().__init__(description, status_code=401)
logger = logging.getLogger(__name__)

View File

@ -140,21 +140,14 @@ class WorkflowService:
)
return db.session.execute(stmt).scalar_one()
def get_draft_workflow(
self, app_model: App, workflow_id: str | None = None, session: Session | None = None
) -> Workflow | None:
def get_draft_workflow(self, app_model: App, workflow_id: str | None = None) -> Workflow | None:
"""
Get draft workflow
When ``session`` is provided, reuse it so callers that already hold a
Session avoid checking out an extra request-scoped ``db.session``
connection. Falls back to ``db.session`` for backward compatibility.
"""
if workflow_id:
return self.get_published_workflow_by_id(app_model, workflow_id, session=session)
return self.get_published_workflow_by_id(app_model, workflow_id)
# fetch draft workflow by app_model
bind = session if session is not None else db.session
workflow = bind.scalar(
workflow = db.session.scalar(
select(Workflow)
.where(
Workflow.tenant_id == app_model.tenant_id,

View File

@ -7,7 +7,6 @@ from importlib import util
from pathlib import Path
from types import ModuleType, SimpleNamespace
from typing import Any
from unittest.mock import MagicMock
import pytest
from flask.views import MethodView
@ -19,15 +18,6 @@ if not hasattr(builtins, "MethodView"):
builtins.MethodView = MethodView # type: ignore[attr-defined]
def _unwrap(func):
bound_self = getattr(func, "__self__", None)
while hasattr(func, "__wrapped__"):
func = func.__wrapped__
if bound_self is not None:
return func.__get__(bound_self, bound_self.__class__)
return func
@pytest.fixture(scope="module")
def app_module():
module_name = "controllers.console.app.app"
@ -405,46 +395,3 @@ def test_app_pagination_aliases_per_page_and_has_next(app_models):
assert len(serialized["data"]) == 2
assert serialized["data"][0]["icon_url"] == "signed:first-icon"
assert serialized["data"][1]["icon_url"] is None
def test_app_list_uses_injected_session_for_draft_workflows(app, app_module, monkeypatch):
api = app_module.AppListApi()
method = _unwrap(api.get)
current_user = SimpleNamespace(id="user-1")
app_item = SimpleNamespace(
id="app-1",
name="Workflow App",
desc_or_prompt="Summary",
mode="workflow",
mode_compatible_with_agent="workflow",
)
app_pagination = SimpleNamespace(page=1, per_page=20, total=1, has_next=False, items=[app_item])
workflow = SimpleNamespace(
id="workflow-1",
app_id="app-1",
walk_nodes=lambda: iter([("trigger-1", {"type": "trigger-webhook"})]),
)
session = MagicMock()
session.execute.return_value.scalars.return_value.all.return_value = [workflow]
scoped_session = SimpleNamespace(execute=MagicMock(side_effect=AssertionError("db.session should not be used")))
monkeypatch.setattr(app_module, "current_account_with_tenant", lambda: (current_user, "tenant-1"))
monkeypatch.setattr(
app_module,
"AppService",
lambda: SimpleNamespace(get_paginate_apps=lambda *_args, **_kwargs: app_pagination),
)
monkeypatch.setattr(
app_module,
"FeatureService",
SimpleNamespace(get_system_features=lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))),
)
monkeypatch.setattr(app_module, "db", SimpleNamespace(session=scoped_session))
with app.test_request_context("/console/api/apps?page=1&limit=20", method="GET"):
response, status = method(session)
assert status == 200
assert response["data"][0]["has_draft_trigger"] is True
session.execute.assert_called_once()
scoped_session.execute.assert_not_called()

View File

@ -1,7 +1,6 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
@ -25,17 +24,10 @@ def _model_config_payload():
def _install_workflow_service(monkeypatch: pytest.MonkeyPatch, workflow):
class _Service:
app_model = None
session = None
def get_draft_workflow(self, app_model, session=None):
self.app_model = app_model
self.session = session
def get_draft_workflow(self, app_model):
return workflow
service = _Service()
monkeypatch.setattr(generator_module, "WorkflowService", lambda: service)
return service
monkeypatch.setattr(generator_module, "WorkflowService", lambda: _Service())
def test_rule_generate_success(app, monkeypatch: pytest.MonkeyPatch) -> None:
@ -76,8 +68,7 @@ def test_instruction_generate_app_not_found(app, monkeypatch: pytest.MonkeyPatch
api = generator_module.InstructionGenerateApi()
method = _unwrap(api.post)
session = MagicMock()
session.get.return_value = None
monkeypatch.setattr(generator_module.db, "session", SimpleNamespace(get=lambda *_args, **_kwargs: None))
with app.test_request_context(
"/console/api/instruction-generate",
@ -89,11 +80,10 @@ def test_instruction_generate_app_not_found(app, monkeypatch: pytest.MonkeyPatch
"model_config": _model_config_payload(),
},
):
response, status = method(session, "t1")
response, status = method("t1")
assert status == 400
assert response["error"] == "app app-1 not found"
session.get.assert_called_once_with(generator_module.App, "app-1")
def test_instruction_generate_workflow_not_found(app, monkeypatch: pytest.MonkeyPatch) -> None:
@ -101,7 +91,7 @@ def test_instruction_generate_workflow_not_found(app, monkeypatch: pytest.Monkey
method = _unwrap(api.post)
app_model = SimpleNamespace(id="app-1")
session = SimpleNamespace(get=lambda *_args, **_kwargs: app_model)
monkeypatch.setattr(generator_module.db, "session", SimpleNamespace(get=lambda *_args, **_kwargs: app_model))
_install_workflow_service(monkeypatch, workflow=None)
with app.test_request_context(
@ -114,7 +104,7 @@ def test_instruction_generate_workflow_not_found(app, monkeypatch: pytest.Monkey
"model_config": _model_config_payload(),
},
):
response, status = method(session, "t1")
response, status = method("t1")
assert status == 400
assert response["error"] == "workflow app-1 not found"
@ -125,7 +115,7 @@ def test_instruction_generate_node_missing(app, monkeypatch: pytest.MonkeyPatch)
method = _unwrap(api.post)
app_model = SimpleNamespace(id="app-1")
session = SimpleNamespace(get=lambda *_args, **_kwargs: app_model)
monkeypatch.setattr(generator_module.db, "session", SimpleNamespace(get=lambda *_args, **_kwargs: app_model))
workflow = SimpleNamespace(graph_dict={"nodes": []})
_install_workflow_service(monkeypatch, workflow=workflow)
@ -140,7 +130,7 @@ def test_instruction_generate_node_missing(app, monkeypatch: pytest.MonkeyPatch)
"model_config": _model_config_payload(),
},
):
response, status = method(session, "t1")
response, status = method("t1")
assert status == 400
assert response["error"] == "node node-1 not found"
@ -151,7 +141,7 @@ def test_instruction_generate_code_node(app, monkeypatch: pytest.MonkeyPatch) ->
method = _unwrap(api.post)
app_model = SimpleNamespace(id="app-1")
session = SimpleNamespace(get=lambda *_args, **_kwargs: app_model)
monkeypatch.setattr(generator_module.db, "session", SimpleNamespace(get=lambda *_args, **_kwargs: app_model))
workflow = SimpleNamespace(
graph_dict={
@ -160,7 +150,7 @@ def test_instruction_generate_code_node(app, monkeypatch: pytest.MonkeyPatch) ->
]
}
)
workflow_service = _install_workflow_service(monkeypatch, workflow=workflow)
_install_workflow_service(monkeypatch, workflow=workflow)
monkeypatch.setattr(generator_module.LLMGenerator, "generate_code", lambda **_kwargs: {"code": "x"})
with app.test_request_context(
@ -173,17 +163,14 @@ def test_instruction_generate_code_node(app, monkeypatch: pytest.MonkeyPatch) ->
"model_config": _model_config_payload(),
},
):
response = method(session, "t1")
response = method("t1")
assert response == {"code": "x"}
assert workflow_service.app_model is app_model
assert workflow_service.session is session
def test_instruction_generate_legacy_modify(app, monkeypatch: pytest.MonkeyPatch) -> None:
api = generator_module.InstructionGenerateApi()
method = _unwrap(api.post)
session = SimpleNamespace()
monkeypatch.setattr(
generator_module.LLMGenerator,
@ -202,7 +189,7 @@ def test_instruction_generate_legacy_modify(app, monkeypatch: pytest.MonkeyPatch
"model_config": _model_config_payload(),
},
):
response = method(session, "t1")
response = method("t1")
assert response == {"instruction": "ok"}
@ -210,7 +197,6 @@ def test_instruction_generate_legacy_modify(app, monkeypatch: pytest.MonkeyPatch
def test_instruction_generate_incompatible_params(app, monkeypatch: pytest.MonkeyPatch) -> None:
api = generator_module.InstructionGenerateApi()
method = _unwrap(api.post)
session = SimpleNamespace()
with app.test_request_context(
"/console/api/instruction-generate",
@ -223,7 +209,7 @@ def test_instruction_generate_incompatible_params(app, monkeypatch: pytest.Monke
"model_config": _model_config_payload(),
},
):
response, status = method(session, "t1")
response, status = method("t1")
assert status == 400
assert response["error"] == "incompatible parameters"

View File

@ -346,19 +346,6 @@ class TestWorkflowService:
assert result == mock_workflow
def test_get_draft_workflow_uses_provided_session(self, workflow_service, mock_db_session):
"""Test get_draft_workflow can reuse an injected SQLAlchemy session."""
app = TestWorkflowAssociatedDataFactory.create_app_mock()
mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock()
session = MagicMock()
session.scalar.return_value = mock_workflow
result = workflow_service.get_draft_workflow(app, session=session)
assert result == mock_workflow
session.scalar.assert_called_once()
mock_db_session.session.scalar.assert_not_called()
def test_get_draft_workflow_returns_none(self, workflow_service, mock_db_session):
"""Test get_draft_workflow returns None when no draft exists."""
app = TestWorkflowAssociatedDataFactory.create_app_mock()
@ -383,21 +370,6 @@ class TestWorkflowService:
assert result == mock_workflow
def test_get_draft_workflow_with_workflow_id_reuses_provided_session(self, workflow_service):
"""Test get_draft_workflow passes an injected session to published workflow lookup."""
app = TestWorkflowAssociatedDataFactory.create_app_mock()
workflow_id = "workflow-123"
session = MagicMock()
mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock(version="v1")
with patch.object(
workflow_service, "get_published_workflow_by_id", return_value=mock_workflow
) as mock_get_published:
result = workflow_service.get_draft_workflow(app, workflow_id=workflow_id, session=session)
assert result == mock_workflow
mock_get_published.assert_called_once_with(app, workflow_id, session=session)
# ==================== Get Published Workflow Tests ====================
# These tests verify retrieval of published workflows (versioned snapshots)

View File

@ -392,14 +392,12 @@ export type McpProviderCreatePayload = {
configuration?: {
[key: string]: unknown
} | null
forward_user_identity?: boolean
headers?: {
[key: string]: unknown
} | null
icon: string
icon_background?: string
icon_type: string
identity_mode?: 'idp_token' | 'off'
name: string
server_identifier: string
server_url: string
@ -412,14 +410,12 @@ export type McpProviderUpdatePayload = {
configuration?: {
[key: string]: unknown
} | null
forward_user_identity?: boolean
headers?: {
[key: string]: unknown
} | null
icon: string
icon_background?: string
icon_type: string
identity_mode?: 'idp_token' | 'off'
name: string
provider_id: string
server_identifier: string

View File

@ -361,12 +361,10 @@ export const zMcpProviderDeletePayload = z.object({
export const zMcpProviderCreatePayload = z.object({
authentication: z.record(z.string(), z.unknown()).nullish(),
configuration: z.record(z.string(), z.unknown()).nullish(),
forward_user_identity: z.boolean().optional().default(false),
headers: z.record(z.string(), z.unknown()).nullish(),
icon: z.string(),
icon_background: z.string().optional().default(''),
icon_type: z.string(),
identity_mode: z.enum(['idp_token', 'off']).optional().default('off'),
name: z.string(),
server_identifier: z.string(),
server_url: z.string(),
@ -378,12 +376,10 @@ export const zMcpProviderCreatePayload = z.object({
export const zMcpProviderUpdatePayload = z.object({
authentication: z.record(z.string(), z.unknown()).nullish(),
configuration: z.record(z.string(), z.unknown()).nullish(),
forward_user_identity: z.boolean().optional().default(false),
headers: z.record(z.string(), z.unknown()).nullish(),
icon: z.string(),
icon_background: z.string().optional().default(''),
icon_type: z.string(),
identity_mode: z.enum(['idp_token', 'off']).optional().default('off'),
name: z.string(),
provider_id: z.string(),
server_identifier: z.string(),

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

@ -145,6 +145,8 @@
"mcp.modal.timeout": "Timeout",
"mcp.modal.timeoutPlaceholder": "30",
"mcp.modal.title": "Add 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.useDynamicClientRegistration": "Use Dynamic Client Registration",
"mcp.noConfigured": "Unconfigured",
"mcp.noTools": "No tools available",

View File

@ -145,6 +145,8 @@
"mcp.modal.timeout": "超时时间",
"mcp.modal.timeoutPlaceholder": "30",
"mcp.modal.title": "添加 MCP 服务 (HTTP)",
"mcp.modal.forwardUserIdentity": "转发用户身份",
"mcp.modal.forwardUserIdentityTip": "将调用用户的已验证 SSO 身份作为 Authorization Bearer token 转发到该 MCP 服务器。需要 Dify Enterprise SSO。",
"mcp.modal.useDynamicClientRegistration": "使用动态客户端注册",
"mcp.noConfigured": "未配置",
"mcp.noTools": "没有可用的工具",

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: {