From 01922f2d028eadd6a66dad905ae29f425992c061 Mon Sep 17 00:00:00 2001 From: Novice Date: Wed, 25 Jun 2025 09:38:50 +0800 Subject: [PATCH] feat: add unique id in mcp tool dsl --- .../console/workspace/tool_providers.py | 12 +++------- api/core/tools/tool_manager.py | 23 ++----------------- ...3fe_add_mcp_server_tool_and_app_server.py} | 14 +++++++---- api/models/model.py | 6 ++++- api/models/tools.py | 3 +++ api/services/tools/mcp_tools_mange_service.py | 22 ++++++++++++++---- api/services/tools/tools_transform_service.py | 9 ++++---- 7 files changed, 45 insertions(+), 44 deletions(-) rename api/migrations/versions/{2025_06_20_1058-9e4b39294dc8_add_mcp_server_tool_and_app_server.py => 2025_06_25_0936-58eb7bdb93fe_add_mcp_server_tool_and_app_server.py} (82%) diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index d3b5f4958c..ef0d23e280 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -630,6 +630,7 @@ class ToolProviderMCPApi(Resource): parser.add_argument("icon", type=str, required=True, nullable=False, location="json") parser.add_argument("icon_type", type=str, required=True, nullable=False, location="json") parser.add_argument("icon_background", type=str, required=False, nullable=True, location="json", default="") + parser.add_argument("server_identifier", type=str, required=True, nullable=False, location="json") args = parser.parse_args() user = current_user if not validators.url(args["server_url"]): @@ -643,6 +644,7 @@ class ToolProviderMCPApi(Resource): icon_type=args["icon_type"], icon_background=args["icon_background"], user_id=user.id, + server_identifier=args["server_identifier"], ) ) @@ -657,21 +659,13 @@ class ToolProviderMCPApi(Resource): parser.add_argument("icon_type", type=str, required=True, nullable=False, location="json") parser.add_argument("icon_background", type=str, required=False, nullable=True, location="json") parser.add_argument("provider_id", type=str, required=True, nullable=False, location="json") + parser.add_argument("server_identifier", type=str, required=True, nullable=False, location="json") args = parser.parse_args() if not validators.url(args["server_url"]): if "[__HIDDEN__]" in args["server_url"]: pass else: raise ValueError("Server URL is not valid.") - MCPToolManageService.update_mcp_provider( - tenant_id=current_user.current_tenant_id, - name=args["name"], - server_url=args["server_url"], - icon=args["icon"], - icon_type=args["icon_type"], - icon_background=args["icon_background"], - provider_id=args["provider_id"], - ) return {"result": "success"} @setup_required diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 71c5c853b3..40c614bc6f 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -414,25 +414,6 @@ class ToolManager: tool_entity.runtime.runtime_parameters.update(runtime_parameters) return tool_entity - @classmethod - def get_tool_runtime_from_mcp( - cls, - tenant_id: str, - provider_id: str, - tool_name: str, - ) -> Tool: - """ - get tool runtime from mcp - """ - return cls.get_tool_runtime( - provider_type=ToolProviderType.MCP, - provider_id=provider_id, - tool_name=tool_name, - tenant_id=tenant_id, - invoke_from=InvokeFrom.SERVICE_API, - tool_invoke_from=ToolInvokeFrom.PLUGIN, - ) - @classmethod def get_hardcoded_provider_icon(cls, provider: str) -> tuple[str, str]: """ @@ -673,7 +654,7 @@ class ToolManager: ) result_providers[f"workflow_provider.{user_provider.name}"] = user_provider if "mcp" in filters: - mcp_providers = MCPToolManageService.retrieve_mcp_tools(tenant_id) + mcp_providers = MCPToolManageService.retrieve_mcp_tools(tenant_id, for_list=True) for mcp_provider in mcp_providers: result_providers[f"mcp_provider.{mcp_provider.name}"] = mcp_provider @@ -724,7 +705,7 @@ class ToolManager: provider: MCPToolProvider | None = ( db.session.query(MCPToolProvider) .filter( - MCPToolProvider.id == provider_id, + MCPToolProvider.server_identifier == provider_id, MCPToolProvider.tenant_id == tenant_id, ) .first() diff --git a/api/migrations/versions/2025_06_20_1058-9e4b39294dc8_add_mcp_server_tool_and_app_server.py b/api/migrations/versions/2025_06_25_0936-58eb7bdb93fe_add_mcp_server_tool_and_app_server.py similarity index 82% rename from api/migrations/versions/2025_06_20_1058-9e4b39294dc8_add_mcp_server_tool_and_app_server.py rename to api/migrations/versions/2025_06_25_0936-58eb7bdb93fe_add_mcp_server_tool_and_app_server.py index 64bb076218..0c98614dd6 100644 --- a/api/migrations/versions/2025_06_20_1058-9e4b39294dc8_add_mcp_server_tool_and_app_server.py +++ b/api/migrations/versions/2025_06_25_0936-58eb7bdb93fe_add_mcp_server_tool_and_app_server.py @@ -1,8 +1,8 @@ """add mcp server tool and app server -Revision ID: 9e4b39294dc8 -Revises: 4474872b0ee6 -Create Date: 2025-06-12 10:58:14.199433 +Revision ID: 58eb7bdb93fe +Revises: 0ab65e1cc7fa +Create Date: 2025-06-25 09:36:07.510570 """ from alembic import op @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '9e4b39294dc8' +revision = '58eb7bdb93fe' down_revision = '0ab65e1cc7fa' branch_labels = None depends_on = None @@ -30,11 +30,14 @@ def upgrade(): sa.Column('parameters', sa.Text(), nullable=False), sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='app_mcp_server_pkey') + sa.PrimaryKeyConstraint('id', name='app_mcp_server_pkey'), + sa.UniqueConstraint('tenant_id', 'app_id', name='unique_app_mcp_server_tenant_app_id'), + sa.UniqueConstraint('tenant_id', 'server_code', name='unique_app_mcp_server_tenant_server_code') ) op.create_table('tool_mcp_providers', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), sa.Column('name', sa.String(length=40), nullable=False), + sa.Column('server_identifier', sa.String(length=24), nullable=False), sa.Column('server_url', sa.Text(), nullable=False), sa.Column('server_url_hash', sa.String(length=64), nullable=False), sa.Column('icon', sa.String(length=255), nullable=True), @@ -47,6 +50,7 @@ def upgrade(): sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), sa.PrimaryKeyConstraint('id', name='tool_mcp_provider_pkey'), sa.UniqueConstraint('tenant_id', 'name', name='unique_mcp_provider_name'), + sa.UniqueConstraint('tenant_id', 'server_identifier', name='unique_mcp_provider_server_identifier'), sa.UniqueConstraint('tenant_id', 'server_url_hash', name='unique_mcp_provider_server_url') ) diff --git a/api/models/model.py b/api/models/model.py index 16c5fcd5e8..035929b6f7 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -1443,7 +1443,11 @@ class EndUser(Base, UserMixin): class AppMCPServer(Base): __tablename__ = "app_mcp_servers" - __table_args__ = (db.PrimaryKeyConstraint("id", name="app_mcp_server_pkey"),) + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="app_mcp_server_pkey"), + db.UniqueConstraint("tenant_id", "app_id", name="unique_app_mcp_server_tenant_app_id"), + db.UniqueConstraint("tenant_id", "server_code", name="unique_app_mcp_server_tenant_server_code"), + ) id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) tenant_id = db.Column(StringUUID, nullable=False) app_id = db.Column(StringUUID, nullable=False) diff --git a/api/models/tools.py b/api/models/tools.py index 632ba0b42c..1266ff1169 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -201,11 +201,14 @@ class MCPToolProvider(Base): db.PrimaryKeyConstraint("id", name="tool_mcp_provider_pkey"), db.UniqueConstraint("tenant_id", "server_url_hash", name="unique_mcp_provider_server_url"), db.UniqueConstraint("tenant_id", "name", name="unique_mcp_provider_name"), + db.UniqueConstraint("tenant_id", "server_identifier", name="unique_mcp_provider_server_identifier"), ) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) # name of the mcp provider name: Mapped[str] = mapped_column(db.String(40), nullable=False) + # server identifier of the mcp provider + server_identifier: Mapped[str] = mapped_column(db.String(24), nullable=False) # encrypted url of the mcp provider server_url: Mapped[str] = mapped_column(db.Text, nullable=False) # hash of server_url for uniqueness check diff --git a/api/services/tools/mcp_tools_mange_service.py b/api/services/tools/mcp_tools_mange_service.py index 0b0c681192..b24ed897b1 100644 --- a/api/services/tools/mcp_tools_mange_service.py +++ b/api/services/tools/mcp_tools_mange_service.py @@ -50,7 +50,14 @@ class MCPToolManageService: @staticmethod def create_mcp_provider( - tenant_id: str, name: str, server_url: str, user_id: str, icon: str, icon_type: str, icon_background: str + tenant_id: str, + name: str, + server_url: str, + user_id: str, + icon: str, + icon_type: str, + icon_background: str, + server_identifier: str, ) -> ToolProviderApiEntity: server_url_hash = hashlib.sha256(server_url.encode()).hexdigest() existing_provider = ( @@ -80,20 +87,24 @@ class MCPToolManageService: authed=False, tools="[]", icon=json.dumps({"content": icon, "background": icon_background}) if icon_type == "emoji" else icon, + server_identifier=server_identifier, ) db.session.add(mcp_tool) db.session.commit() return ToolTransformService.mcp_provider_to_user_provider(mcp_tool) @staticmethod - def retrieve_mcp_tools(tenant_id: str) -> list[ToolProviderApiEntity]: + def retrieve_mcp_tools(tenant_id: str, for_list: bool = False) -> list[ToolProviderApiEntity]: mcp_providers = ( db.session.query(MCPToolProvider) .filter(MCPToolProvider.tenant_id == tenant_id) .order_by(MCPToolProvider.name) .all() ) - return [ToolTransformService.mcp_provider_to_user_provider(mcp_provider) for mcp_provider in mcp_providers] + return [ + ToolTransformService.mcp_provider_to_user_provider(mcp_provider, for_list=for_list) + for mcp_provider in mcp_providers + ] @classmethod def list_mcp_tool_from_remote_server(cls, tenant_id: str, provider_id: str): @@ -123,6 +134,7 @@ class MCPToolManageService: updated_at=int(mcp_provider.updated_at.timestamp()), description=I18nObject(en_US="", zh_Hans=""), label=I18nObject(en_US=mcp_provider.name, zh_Hans=mcp_provider.name), + plugin_unique_identifier=mcp_provider.server_identifier, ) @classmethod @@ -150,6 +162,7 @@ class MCPToolManageService: icon: str, icon_type: str, icon_background: str, + server_identifier: str, ): mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) if mcp_provider is None: @@ -158,6 +171,8 @@ class MCPToolManageService: mcp_provider.icon = ( json.dumps({"content": icon, "background": icon_background}) if icon_type == "emoji" else icon ) + mcp_provider.server_identifier = server_identifier + if "[__HIDDEN__]" in server_url: db.session.commit() return @@ -182,7 +197,6 @@ class MCPToolManageService: mcp_provider.tools = "[]" mcp_provider.encrypted_credentials = "{}" mcp_provider.server_url_hash = server_url_hash - db.session.commit() except IntegrityError as e: db.session.rollback() diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index dbd042cb9d..1298ddc95e 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -46,14 +46,15 @@ class ToolTransformService: if provider_type == ToolProviderType.BUILT_IN.value: return str(url_prefix / "builtin" / provider_name / "icon") - elif provider_type in {ToolProviderType.API.value, ToolProviderType.WORKFLOW.value, ToolProviderType.MCP.value}: + elif provider_type in {ToolProviderType.API.value, ToolProviderType.WORKFLOW.value}: try: if isinstance(icon, str): return cast(dict, json.loads(icon)) return icon except Exception: return {"background": "#252525", "content": "\ud83d\ude01"} - + elif provider_type == ToolProviderType.MCP.value: + return icon return "" @staticmethod @@ -189,11 +190,11 @@ class ToolTransformService: ) @staticmethod - def mcp_provider_to_user_provider(db_provider: MCPToolProvider) -> ToolProviderApiEntity: + def mcp_provider_to_user_provider(db_provider: MCPToolProvider, for_list: bool = False) -> ToolProviderApiEntity: from services.tools.mcp_tools_mange_service import MCPToolManageService return ToolProviderApiEntity( - id=db_provider.id, + id=db_provider.server_identifier if not for_list else db_provider.id, author=db_provider.user.name if db_provider.user else "Anonymous", name=db_provider.name, icon=db_provider.provider_icon,