├── .difyignore ├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── .gitignore ├── GUIDE.md ├── PRIVACY.md ├── README.md ├── _assets ├── add_endpoint.png ├── add_tool_list.png ├── cherry_studio_mcp_sse.png ├── cherry_studio_mcp_streamable_http.png ├── edit_tool.png ├── icon.svg ├── install_plugin_via_github.png ├── mcp_sse_url.png ├── mcp_streamable_http_url.png ├── mcp_urls.png └── save_endpoint.png ├── endpoints ├── mcp_get.py ├── mcp_get.yaml ├── mcp_post.py ├── mcp_post.yaml ├── messages.py ├── messages.yaml ├── sse.py └── sse.yaml ├── group └── mcp_compat_dify_tools.yaml ├── main.py ├── manifest.yaml └── requirements.txt /.difyignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | share/python-wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | cover/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | db.sqlite3-journal 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | .pybuilder/ 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | # For a library or package, you might want to ignore these files since the code is 84 | # intended to run in multiple environments; otherwise, check them in: 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | Pipfile.lock 93 | 94 | # UV 95 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 96 | # This is especially recommended for binary packages to ensure reproducibility, and is more 97 | # commonly ignored for libraries. 98 | uv.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | .idea/ 166 | 167 | # Vscode 168 | .vscode/ 169 | 170 | # Git 171 | .git/ 172 | .gitignore 173 | 174 | # Mac 175 | .DS_Store 176 | 177 | # Windows 178 | Thumbs.db 179 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INSTALL_METHOD=remote 2 | REMOTE_INSTALL_HOST=debug.dify.ai 3 | REMOTE_INSTALL_PORT=5003 4 | REMOTE_INSTALL_KEY=********-****-****-****-************ 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🕷️ Bug report Bug报告" 2 | description: "Report errors or unexpected behavior. 创建一个 Bug 报告以帮助我们改进。" 3 | labels: 4 | - bug 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: "Please confirm before submission 在提交之前,请确认" 9 | options: 10 | - label: | 11 | I have searched for existing issues [search for existing issues](https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools/issues), including closed ones. 12 | 我已经搜索了现有问题[搜索现有问题](https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools/issues),包括已关闭的问题。" 13 | required: true 14 | - type: input 15 | attributes: 16 | label: "Dify version Dify版本" 17 | validations: 18 | required: true 19 | - type: input 20 | attributes: 21 | label: "Plugin version 插件版本" 22 | validations: 23 | required: true 24 | - type: dropdown 25 | attributes: 26 | label: HTTP with SSE or Streamable HTTP 27 | description: Which transports? 哪种传输? 28 | multiple: true 29 | options: 30 | - HTTP with SSE 31 | - Streamable HTTP 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: "Problem description 问题描述" 37 | description: "Please describe the problem you have encountered clearly and concisely. 请清晰简洁地描述你遇到的问题。" 38 | validations: 39 | required: true 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "\U0001F4E7 Discussions 讨论" 4 | url: https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools/discussions 5 | about: General discussions and request help. 一般性讨论和请求帮助。 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "⭐ Feature or enhancement request 特性或增强请求" 2 | description: Propose something new. 提出一些新的建议。 3 | labels: 4 | - enhancement 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: "Please confirm before submission 在提交之前,请确认" 9 | options: 10 | - label: | 11 | I have searched for existing issues [search for existing issues](https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools/issues), including closed ones. 12 | 我已经搜索了现有问题[搜索现有问题](https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools/issues),包括已关闭的问题。" 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Is this request related to a challenge you're experiencing? Tell me about your story. 这个请求与你正在经历的挑战有关吗?给我讲讲你的故事。 17 | placeholder: | 18 | Please describe the specific scenario or problem you're facing as clearly as possible. For instance "I was trying to use [feature] for [specific task], and [what happened]... It was frustrating because...." 19 | 请尽可能清楚地描述你所面临的具体场景或问题。例如:“我试图将[功能]用于[特定任务],但[发生了什么]……这很令人沮丧,因为....” 20 | validations: 21 | required: true 22 | - type: checkboxes 23 | attributes: 24 | label: Can you help us with this feature? 你能帮我们实现这个功能吗? 25 | description: Let us know! This is not a commitment, but a starting point for collaboration. 让我们知道!这不是承诺,而是合作的起点。 26 | options: 27 | - label: I am interested in contributing to this feature. 我有兴趣为这个特性做贡献。 28 | required: false 29 | - type: markdown 30 | attributes: 31 | value: Please limit one request per issue. 请限制每个问题一个请求。 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | .idea/ 169 | 170 | # Vscode 171 | .vscode/ 172 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | ## User Guide of how to develop a Dify Plugin 2 | 3 | Hi there, looks like you have already created a Plugin, now let's get you started with the development! 4 | 5 | ### Choose a Plugin type you want to develop 6 | 7 | Before start, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify: 8 | - **Tool**: Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task. 9 | - **Model**: Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities. 10 | - **Endpoint**: Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code. 11 | 12 | Based on the ability you want to extend, we have divided the Plugin into three types: **Tool**, **Model**, and **Extension**. 13 | 14 | - **Tool**: It's a tool provider, but not only limited to tools, you can implement an endpoint there, for example, you need both `Sending Message` and `Receiving Message` if you are building a Discord Bot, **Tool** and **Endpoint** are both required. 15 | - **Model**: Just a model provider, extending others is not allowed. 16 | - **Extension**: Other times, you may only need a simple http service to extend the functionalities, **Extension** is the right choice for you. 17 | 18 | I believe you have chosen the right type for your Plugin while creating it, if not, you can change it later by modifying the `manifest.yaml` file. 19 | 20 | ### Manifest 21 | 22 | Now you can edit the `manifest.yaml` file to describe your Plugin, here is the basic structure of it: 23 | 24 | - version(version, required):Plugin's version 25 | - type(type, required):Plugin's type, currently only supports `plugin`, future support `bundle` 26 | - author(string, required):Author, it's the organization name in Marketplace and should also equals to the owner of the repository 27 | - label(label, required):Multi-language name 28 | - created_at(RFC3339, required):Creation time, Marketplace requires that the creation time must be less than the current time 29 | - icon(asset, required):Icon path 30 | - resource (object):Resources to be applied 31 | - memory (int64):Maximum memory usage, mainly related to resource application on SaaS for serverless, unit bytes 32 | - permission(object):Permission application 33 | - tool(object):Reverse call tool permission 34 | - enabled (bool) 35 | - model(object):Reverse call model permission 36 | - enabled(bool) 37 | - llm(bool) 38 | - text_embedding(bool) 39 | - rerank(bool) 40 | - tts(bool) 41 | - speech2text(bool) 42 | - moderation(bool) 43 | - node(object):Reverse call node permission 44 | - enabled(bool) 45 | - endpoint(object):Allow to register endpoint permission 46 | - enabled(bool) 47 | - app(object):Reverse call app permission 48 | - enabled(bool) 49 | - storage(object):Apply for persistent storage permission 50 | - enabled(bool) 51 | - size(int64):Maximum allowed persistent memory, unit bytes 52 | - plugins(object, required):Plugin extension specific ability yaml file list, absolute path in the plugin package, if you need to extend the model, you need to define a file like openai.yaml, and fill in the path here, and the file on the path must exist, otherwise the packaging will fail. 53 | - Format 54 | - tools(list[string]): Extended tool suppliers, as for the detailed format, please refer to [Tool Guide](https://docs.dify.ai/plugins/schema-definition/tool) 55 | - models(list[string]):Extended model suppliers, as for the detailed format, please refer to [Model Guide](https://docs.dify.ai/plugins/schema-definition/model) 56 | - endpoints(list[string]):Extended Endpoints suppliers, as for the detailed format, please refer to [Endpoint Guide](https://docs.dify.ai/plugins/schema-definition/endpoint) 57 | - Restrictions 58 | - Not allowed to extend both tools and models 59 | - Not allowed to have no extension 60 | - Not allowed to extend both models and endpoints 61 | - Currently only supports up to one supplier of each type of extension 62 | - meta(object) 63 | - version(version, required):manifest format version, initial version 0.0.1 64 | - arch(list[string], required):Supported architectures, currently only supports amd64 arm64 65 | - runner(object, required):Runtime configuration 66 | - language(string):Currently only supports python 67 | - version(string):Language version, currently only supports 3.12 68 | - entrypoint(string):Program entry, in python it should be main 69 | 70 | ### Install Dependencies 71 | 72 | - First of all, you need a Python 3.11+ environment, as our SDK requires that. 73 | - Then, install the dependencies: 74 | ```bash 75 | pip install -r requirements.txt 76 | ``` 77 | - If you want to add more dependencies, you can add them to the `requirements.txt` file, once you have set the runner to python in the `manifest.yaml` file, `requirements.txt` will be automatically generated and used for packaging and deployment. 78 | 79 | ### Implement the Plugin 80 | 81 | Now you can start to implement your Plugin, by following these examples, you can quickly understand how to implement your own Plugin: 82 | 83 | - [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai): best practice for model provider 84 | - [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google): a simple example for tool provider 85 | - [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko): a funny example for endpoint group 86 | 87 | ### Test and Debug the Plugin 88 | 89 | You may already noticed that a `.env.example` file in the root directory of your Plugin, just copy it to `.env` and fill in the corresponding values, there are some environment variables you need to set if you want to debug your Plugin locally. 90 | 91 | - `INSTALL_METHOD`: Set this to `remote`, your plugin will connect to a Dify instance through the network. 92 | - `REMOTE_INSTALL_HOST`: The host of your Dify instance, you can use our SaaS instance `https://debug.dify.ai`, or self-hosted Dify instance. 93 | - `REMOTE_INSTALL_PORT`: The port of your Dify instance, default is 5003 94 | - `REMOTE_INSTALL_KEY`: You should get your debugging key from the Dify instance you used, at the right top of the plugin management page, you can see a button with a `debug` icon, click it and you will get the key. 95 | 96 | Run the following command to start your Plugin: 97 | 98 | ```bash 99 | python -m main 100 | ``` 101 | 102 | Refresh the page of your Dify instance, you should be able to see your Plugin in the list now, but it will be marked as `debugging`, you can use it normally, but not recommended for production. 103 | 104 | ### Package the Plugin 105 | 106 | After all, just package your Plugin by running the following command: 107 | 108 | ```bash 109 | dify-plugin plugin package ./ROOT_DIRECTORY_OF_YOUR_PLUGIN 110 | ``` 111 | 112 | you will get a `plugin.difypkg` file, that's all, you can submit it to the Marketplace now, look forward to your Plugin being listed! 113 | 114 | 115 | ## User Privacy Policy 116 | 117 | Please fill in the privacy policy of the plugin if you want to make it published on the Marketplace, refer to [PRIVACY.md](PRIVACY.md) for more details. -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | This tool is designed with privacy in mind and does not collect any user data. We are committed to maintaining your privacy and ensuring your data remains secure. 4 | 5 | ## Data Collection 6 | 7 | - **No Personal Information**: We do not collect, store, or process any personal information. 8 | - **No Usage Data**: We do not track or monitor how you use the tool. 9 | - **No Analytics**: We do not implement any analytics or tracking mechanisms. 10 | 11 | ## Third-Party Services 12 | 13 | This tool does not integrate with or utilize any third-party services that might collect user data. 14 | 15 | ## Changes to Privacy Policy 16 | 17 | If there are any changes to our privacy practices, we will update this document accordingly. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dify 1.0 Plugin Convert your Dify tools's API to MCP compatible API 2 | 3 | **Author:** [Junjie.M](https://github.com/junjiem) 4 | **Type:** extension 5 | **Github Repo:** [https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools](https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools) 6 | **Github Issues:** [issues](https://github.com/junjiem/dify-plugin-mcp_compat_dify_tools/issues) 7 | 8 | 9 | Reference to https://github.com/hjlarry/dify-plugin-mcp_server 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | > **Note:** must be dify 1.2.0 and above. 18 | 19 | > **注:** 必须是 dify 1.2.0及以上版本。 20 | 21 | Convert your Dify tools's API to MCP compatible API (**Currently, two transports are supported: `HTTP with SSE` and `Streamable HTTP`**) 22 | 23 | 将您的 Dify 工具的 API 转换为 MCP 兼容 API (**目前已支持 `HTTP with SSE` 和 `Streamable HTTP` 两种传输方式**) 24 | 25 | 26 | #### 1. Add a endpoint and add tool list. 添加API端点并添加工具列表。 27 | 28 | ![add_endpoint](./_assets/add_endpoint.png) 29 | 30 | ![add_tool_list](./_assets/add_tool_list.png) 31 | 32 | ![edit_tool](./_assets/edit_tool.png) 33 | 34 | ![save_endpoint](./_assets/save_endpoint.png) 35 | 36 | > **Note:** The modification of the tool list for existing endpoint will take effect only after the endpoint are disabled and then enabled. 37 | 38 | > **注:** 对已有 API 端点进行工具列表的修改需要停用再启用 API 端点后才会生效。 39 | 40 | 41 | #### 2. Copy the endpoint url to your MCP client, like `Cherry Studio` 复制端点url到您的MCP客户端,如 `Cherry Studio` 42 | 43 | ![mcp_urls](./_assets/mcp_urls.png) 44 | 45 | 46 | ##### **Option 1:** Use the newest `Streamable HTTP` transport (**Recommended**) 使用最新的`Streamable HTTP`传输方式(**推荐**) 47 | 48 | ![mcp_streamable_http_url](./_assets/mcp_streamable_http_url.png) 49 | 50 | ![cherry_studio_mcp_streamable_http](./_assets/cherry_studio_mcp_streamable_http.png) 51 | 52 | **OK!** 53 | 54 | 55 | ##### **Option 2:** Use the legacy `HTTP with SSE` transport 使用过时的`HTTP with SSE`传输方式 56 | 57 | ![mcp_sse_url](./_assets/mcp_sse_url.png) 58 | 59 | ![cherry_studio_mcp_sse](./_assets/cherry_studio_mcp_sse.png) 60 | 61 | **OK!** 62 | 63 | 64 | 65 | --- 66 | 67 | 68 | 69 | ### Installing Plugins via GitHub 通过 GitHub 安装插件 70 | 71 | Can install the plugin using the GitHub repository address. Visit the Dify platform's plugin management page, choose to install via GitHub, enter the repository address, select version number and package file to complete installation. 72 | 73 | 可以通过 GitHub 仓库地址安装该插件。访问 Dify 平台的插件管理页,选择通过 GitHub 安装插件,输入仓库地址后,选择版本号和包文件完成安装。 74 | 75 | ![install_plugin_via_github](_assets/install_plugin_via_github.png) 76 | 77 | 78 | 79 | --- 80 | 81 | 82 | 83 | ### FAQ 84 | 85 | #### 1. How to Handle Errors When Installing Plugins? 安装插件时遇到异常应如何处理? 86 | 87 | **Issue**: If you encounter the error message: plugin verification has been enabled, and the plugin you want to install has a bad signature, how to handle the issue? 88 | 89 | **Solution**: Add the following line to the end of your .env configuration file: FORCE_VERIFYING_SIGNATURE=false 90 | Once this field is added, the Dify platform will allow the installation of all plugins that are not listed (and thus not verified) in the Dify Marketplace. 91 | 92 | **问题描述**:安装插件时遇到异常信息:plugin verification has been enabled, and the plugin you want to install has a bad signature,应该如何处理? 93 | 94 | **解决办法**:在 .env 配置文件的末尾添加 FORCE_VERIFYING_SIGNATURE=false 字段即可解决该问题。 95 | 添加该字段后,Dify 平台将允许安装所有未在 Dify Marketplace 上架(审核)的插件,可能存在安全隐患。 96 | 97 | 98 | #### 2. How to install the offline version 如何安装离线版本 99 | 100 | Scripting tool for downloading Dify plugin package from Dify Marketplace and Github and repackaging [true] offline package (contains dependencies, no need to be connected to the Internet). 101 | 102 | 从Dify市场和Github下载Dify插件包并重新打【真】离线包(包含依赖,不需要再联网)的脚本工具。 103 | 104 | Github Repo: https://github.com/junjiem/dify-plugin-repackaging 105 | 106 | -------------------------------------------------------------------------------- /_assets/add_endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/add_endpoint.png -------------------------------------------------------------------------------- /_assets/add_tool_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/add_tool_list.png -------------------------------------------------------------------------------- /_assets/cherry_studio_mcp_sse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/cherry_studio_mcp_sse.png -------------------------------------------------------------------------------- /_assets/cherry_studio_mcp_streamable_http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/cherry_studio_mcp_streamable_http.png -------------------------------------------------------------------------------- /_assets/edit_tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/edit_tool.png -------------------------------------------------------------------------------- /_assets/icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /_assets/install_plugin_via_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/install_plugin_via_github.png -------------------------------------------------------------------------------- /_assets/mcp_sse_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/mcp_sse_url.png -------------------------------------------------------------------------------- /_assets/mcp_streamable_http_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/mcp_streamable_http_url.png -------------------------------------------------------------------------------- /_assets/mcp_urls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/mcp_urls.png -------------------------------------------------------------------------------- /_assets/save_endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junjiem/dify-plugin-mcp_compat_dify_tools/04639469d71910ab590a03add219cf7061321d5c/_assets/save_endpoint.png -------------------------------------------------------------------------------- /endpoints/mcp_get.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | from dify_plugin import Endpoint 4 | from werkzeug import Request, Response 5 | 6 | 7 | class McpGetEndpoint(Endpoint): 8 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 9 | """ 10 | Streamable HTTP in Dify is a lightweight design, 11 | it only supported POST and don't support Server-Sent Events (SSE). 12 | """ 13 | response = { 14 | "jsonrpc": "2.0", 15 | "id": None, 16 | "error": { 17 | "code": -32000, 18 | "message": "Not support make use of Server-Sent Events (SSE) to stream multiple server messages." 19 | }, 20 | } 21 | 22 | return Response(response, status=405, content_type="application/json") 23 | -------------------------------------------------------------------------------- /endpoints/mcp_get.yaml: -------------------------------------------------------------------------------- 1 | path: "/mcp" 2 | method: "GET" 3 | extra: 4 | python: 5 | source: "endpoints/mcp_get.py" 6 | -------------------------------------------------------------------------------- /endpoints/mcp_post.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import uuid 4 | from typing import Mapping, cast, Any 5 | 6 | from dify_plugin import Endpoint 7 | from dify_plugin.entities import I18nObject 8 | from dify_plugin.entities.tool import ToolParameter, ToolProviderType, ToolInvokeMessage, ToolDescription 9 | from dify_plugin.interfaces.agent import ToolEntity, AgentToolIdentity 10 | from pydantic import BaseModel 11 | from werkzeug import Request, Response 12 | 13 | 14 | class EndpointParams(BaseModel): 15 | tools: list[ToolEntity] | None 16 | 17 | 18 | class McpPostEndpoint(Endpoint): 19 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 20 | """ 21 | The simplest MCP Streamable HTTP transport implementation. 22 | 23 | 1. not validate the `Origin` header 24 | 2. not authentication 25 | 3. not valid session id 26 | 4. not support Server-Sent Events (SSE) 27 | """ 28 | 29 | session_id = r.args.get('session_id') 30 | data = r.json 31 | method = data.get("method") 32 | 33 | print("===============tools==============") 34 | print(settings.get("tools")) 35 | 36 | if method == "initialize": 37 | session_id = str(uuid.uuid4()).replace("-", "") 38 | response = { 39 | "jsonrpc": "2.0", 40 | "id": data.get("id"), 41 | "result": { 42 | "protocolVersion": "2024-11-05", 43 | "capabilities": { 44 | "tools": {}, 45 | }, 46 | "serverInfo": { 47 | "name": "MCP Compatible Dify Tools", 48 | "version": "1.0.0" 49 | }, 50 | }, 51 | } 52 | headers = {"mcp-session-id": session_id} 53 | return Response( 54 | json.dumps(response), 55 | status=200, 56 | content_type="application/json", 57 | headers=headers, 58 | ) 59 | elif method == "notifications/initialized": 60 | return Response("", status=202, content_type="application/json") 61 | 62 | elif method == "tools/list": 63 | try: 64 | tools: list[ToolEntity] = self._init_tools(settings.get("tools")) 65 | 66 | mcp_tools = self._init_mcp_tools(tools) 67 | 68 | response = { 69 | "jsonrpc": "2.0", 70 | "id": data.get("id"), 71 | "result": { 72 | "tools": mcp_tools 73 | } 74 | } 75 | except Exception as e: 76 | response = { 77 | "jsonrpc": "2.0", 78 | "id": data.get("id"), 79 | "error": { 80 | "code": -32000, 81 | "message": str(e) 82 | } 83 | } 84 | elif method == "tools/call": 85 | try: 86 | tools: list[ToolEntity] = self._init_tools(settings.get("tools")) 87 | tool_instances = {tool.identity.name: tool for tool in tools} if tools else {} 88 | 89 | tool_name = data.get("params", {}).get("name") 90 | arguments = data.get("params", {}).get("arguments", {}) 91 | 92 | tool_instance = tool_instances.get(tool_name) 93 | if tool_instance: 94 | result = self._invoke_tool(tool_instance, arguments) 95 | else: 96 | raise ValueError(f"Unknown tool: {tool_name}") 97 | 98 | response = { 99 | "jsonrpc": "2.0", 100 | "id": data.get("id"), 101 | "result": { 102 | "content": [{"type": "text", "text": result}], 103 | "isError": False 104 | } 105 | } 106 | except Exception as e: 107 | response = { 108 | "jsonrpc": "2.0", 109 | "id": data.get("id"), 110 | "error": { 111 | "code": -32000, 112 | "message": str(e) 113 | } 114 | } 115 | else: 116 | response = { 117 | "jsonrpc": "2.0", 118 | "id": data.get("id"), 119 | "error": { 120 | "code": -32001, 121 | "message": f"Unsupported method: {method}" 122 | } 123 | } 124 | 125 | return Response( 126 | json.dumps(response), status=200, content_type="application/json" 127 | ) 128 | 129 | def _init_tools(self, tools_param_value) -> list[ToolEntity]: 130 | """ 131 | init ToolEntity list 132 | """ 133 | 134 | result: list[ToolEntity] = [] 135 | 136 | value = cast(list[dict[str, Any]], tools_param_value) 137 | value = [tool for tool in value if tool.get("enabled", False)] 138 | 139 | for tool in value: 140 | tool_type = tool["type"] 141 | tool_name = tool["tool_name"] 142 | tool_label = tool["tool_label"] 143 | tool_description = tool.get("tool_description", None) 144 | extra_description = tool.get("extra", {}).get("description", None) 145 | provider_name = tool["provider_name"] 146 | schemas = tool.get("schemas", []) 147 | settings = tool.get("settings", {}) 148 | 149 | identity = AgentToolIdentity( 150 | author="Dify", 151 | name=tool_name, 152 | label=I18nObject(en_US=tool_label), 153 | provider=provider_name, 154 | ) 155 | 156 | llm_description = ( 157 | extra_description 158 | if extra_description else tool_description 159 | if tool_description else tool_label 160 | ) 161 | description = ToolDescription( 162 | human=I18nObject(en_US=llm_description), 163 | llm=llm_description, 164 | ) 165 | 166 | provider_type = ToolProviderType.BUILT_IN 167 | if tool_type == "api": 168 | provider_type = ToolProviderType.API 169 | elif tool_type == "workflow": 170 | provider_type = ToolProviderType.WORKFLOW 171 | 172 | parameters = [] 173 | for schema in schemas: 174 | parameters.append(ToolParameter(**schema)) 175 | 176 | runtime_parameters = {} 177 | for parameter_name, parameter_value in settings.items(): 178 | runtime_parameters[parameter_name] = parameter_value.get("value") 179 | 180 | tool_entity = ToolEntity( 181 | identity=identity, 182 | parameters=parameters, 183 | description=description, 184 | provider_type=provider_type, 185 | runtime_parameters=runtime_parameters, 186 | ) 187 | 188 | result.append(tool_entity) 189 | 190 | return result 191 | 192 | def _init_mcp_tools(self, tools: list[ToolEntity] | None) -> list[dict]: 193 | """ 194 | Init mcp tools 195 | """ 196 | 197 | mcp_tools = [] 198 | for tool in tools or []: 199 | try: 200 | mcp_tool = self._convert_tool_to_mcp_tool(tool) 201 | except Exception: 202 | logging.exception("Failed to convert Dify tool to MCP tool") 203 | continue 204 | 205 | mcp_tools.append(mcp_tool) 206 | 207 | return mcp_tools 208 | 209 | def _convert_tool_to_mcp_tool(self, tool: ToolEntity) -> dict: 210 | """ 211 | convert tool to prompt message tool 212 | """ 213 | mcp_tool = { 214 | "name": tool.identity.name, 215 | "description": tool.description.llm if tool.description else "", 216 | "inputSchema": { 217 | "type": "object", 218 | "properties": {}, 219 | "required": [] 220 | } 221 | } 222 | 223 | parameters = tool.parameters 224 | for parameter in parameters: 225 | if parameter.form != ToolParameter.ToolParameterForm.LLM: 226 | continue 227 | 228 | parameter_type = parameter.type 229 | if parameter.type in { 230 | ToolParameter.ToolParameterType.FILE, 231 | ToolParameter.ToolParameterType.FILES, 232 | }: 233 | continue 234 | enum = [] 235 | if parameter.type == ToolParameter.ToolParameterType.SELECT: 236 | enum = [option.value for option in parameter.options] if parameter.options else [] 237 | 238 | mcp_tool["inputSchema"]["properties"][parameter.name] = { 239 | "type": parameter_type, 240 | "description": parameter.llm_description or "", 241 | } 242 | 243 | if len(enum) > 0: 244 | mcp_tool["inputSchema"]["properties"][parameter.name]["enum"] = enum 245 | 246 | if parameter.required: 247 | mcp_tool["inputSchema"]["required"].append(parameter.name) 248 | 249 | return mcp_tool 250 | 251 | def _invoke_tool(self, tool: ToolEntity, tool_call_args) -> str: 252 | """ 253 | invoke tool 254 | """ 255 | 256 | tool_invoke_responses = self.session.tool.invoke( 257 | provider_type=ToolProviderType(tool.provider_type), 258 | provider=tool.identity.provider, 259 | tool_name=tool.identity.name, 260 | parameters={**tool.runtime_parameters, **tool_call_args}, 261 | ) 262 | 263 | result = "" 264 | for response in tool_invoke_responses: 265 | if response.type == ToolInvokeMessage.MessageType.TEXT: 266 | result += cast(ToolInvokeMessage.TextMessage, response.message).text 267 | elif response.type == ToolInvokeMessage.MessageType.LINK: 268 | result += ( 269 | f"result link: {cast(ToolInvokeMessage.TextMessage, response.message).text}." 270 | + " please tell user to check it." 271 | ) 272 | elif response.type in { 273 | ToolInvokeMessage.MessageType.IMAGE_LINK, 274 | ToolInvokeMessage.MessageType.IMAGE, 275 | }: 276 | result += f"Not support message type: {response.type}." 277 | elif response.type == ToolInvokeMessage.MessageType.JSON: 278 | text = json.dumps( 279 | cast(ToolInvokeMessage.JsonMessage, response.message).json_object, 280 | ensure_ascii=False, 281 | ) 282 | result += f"tool response: {text}." 283 | else: 284 | result += f"tool response: {response.message!r}." 285 | 286 | return result 287 | -------------------------------------------------------------------------------- /endpoints/mcp_post.yaml: -------------------------------------------------------------------------------- 1 | path: "/mcp" 2 | method: "POST" 3 | extra: 4 | python: 5 | source: "endpoints/mcp_post.py" 6 | -------------------------------------------------------------------------------- /endpoints/messages.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Mapping, cast, Any 4 | 5 | from dify_plugin import Endpoint 6 | from dify_plugin.entities import I18nObject 7 | from dify_plugin.entities.tool import ToolParameter, ToolProviderType, ToolInvokeMessage, ToolDescription 8 | from dify_plugin.interfaces.agent import ToolEntity, AgentToolIdentity 9 | from pydantic import BaseModel 10 | from werkzeug import Request, Response 11 | 12 | 13 | class EndpointParams(BaseModel): 14 | tools: list[ToolEntity] | None 15 | 16 | 17 | class MessageEndpoint(Endpoint): 18 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 19 | """ 20 | Invokes the endpoint with the given request. 21 | """ 22 | 23 | session_id = r.args.get('session_id') 24 | data = r.json 25 | method = data.get("method") 26 | 27 | print("===============tools==============") 28 | print(settings.get("tools")) 29 | 30 | if method == "initialize": 31 | response = { 32 | "jsonrpc": "2.0", 33 | "id": data.get("id"), 34 | "result": { 35 | "protocolVersion": "2024-11-05", 36 | "capabilities": { 37 | "experimental": {}, 38 | "prompts": {"listChanged": False}, 39 | "resources": { 40 | "subscribe": False, 41 | "listChanged": False 42 | }, 43 | "tools": {"listChanged": False} 44 | }, 45 | "serverInfo": { 46 | "name": "MCP Compatible Dify Tools", 47 | "version": "1.0.0" 48 | } 49 | } 50 | } 51 | 52 | elif method == "notifications/initialized": 53 | return Response("", status=202, content_type="application/json") 54 | 55 | elif method == "tools/list": 56 | try: 57 | tools: list[ToolEntity] = self._init_tools(settings.get("tools")) 58 | 59 | mcp_tools = self._init_mcp_tools(tools) 60 | 61 | response = { 62 | "jsonrpc": "2.0", 63 | "id": data.get("id"), 64 | "result": { 65 | "tools": mcp_tools 66 | } 67 | } 68 | except Exception as e: 69 | response = { 70 | "jsonrpc": "2.0", 71 | "id": data.get("id"), 72 | "error": { 73 | "code": -32000, 74 | "message": str(e) 75 | } 76 | } 77 | elif method == "tools/call": 78 | try: 79 | tools: list[ToolEntity] = self._init_tools(settings.get("tools")) 80 | tool_instances = {tool.identity.name: tool for tool in tools} if tools else {} 81 | 82 | tool_name = data.get("params", {}).get("name") 83 | arguments = data.get("params", {}).get("arguments", {}) 84 | 85 | tool_instance = tool_instances.get(tool_name) 86 | if tool_instance: 87 | result = self._invoke_tool(tool_instance, arguments) 88 | else: 89 | raise ValueError(f"Unknown tool: {tool_name}") 90 | 91 | response = { 92 | "jsonrpc": "2.0", 93 | "id": data.get("id"), 94 | "result": { 95 | "content": [{"type": "text", "text": result}], 96 | "isError": False 97 | } 98 | } 99 | except Exception as e: 100 | response = { 101 | "jsonrpc": "2.0", 102 | "id": data.get("id"), 103 | "error": { 104 | "code": -32000, 105 | "message": str(e) 106 | } 107 | } 108 | else: 109 | response = { 110 | "jsonrpc": "2.0", 111 | "id": data.get("id"), 112 | "error": { 113 | "code": -32001, 114 | "message": f"Unsupported method: {method}" 115 | } 116 | } 117 | 118 | self.session.storage.set(session_id, json.dumps(response).encode()) 119 | return Response("", status=202, content_type="application/json") 120 | 121 | def _init_tools(self, tools_param_value) -> list[ToolEntity]: 122 | """ 123 | init ToolEntity list 124 | """ 125 | 126 | result: list[ToolEntity] = [] 127 | 128 | value = cast(list[dict[str, Any]], tools_param_value) 129 | value = [tool for tool in value if tool.get("enabled", False)] 130 | 131 | for tool in value: 132 | tool_type = tool["type"] 133 | tool_name = tool["tool_name"] 134 | tool_label = tool["tool_label"] 135 | tool_description = tool.get("tool_description", None) 136 | extra_description = tool.get("extra", {}).get("description", None) 137 | provider_name = tool["provider_name"] 138 | schemas = tool.get("schemas", []) 139 | settings = tool.get("settings", {}) 140 | 141 | identity = AgentToolIdentity( 142 | author="Dify", 143 | name=tool_name, 144 | label=I18nObject(en_US=tool_label), 145 | provider=provider_name, 146 | ) 147 | 148 | llm_description = ( 149 | extra_description 150 | if extra_description else tool_description 151 | if tool_description else tool_label 152 | ) 153 | description = ToolDescription( 154 | human=I18nObject(en_US=llm_description), 155 | llm=llm_description, 156 | ) 157 | 158 | provider_type = ToolProviderType.BUILT_IN 159 | if tool_type == "api": 160 | provider_type = ToolProviderType.API 161 | elif tool_type == "workflow": 162 | provider_type = ToolProviderType.WORKFLOW 163 | 164 | parameters = [] 165 | for schema in schemas: 166 | parameters.append(ToolParameter(**schema)) 167 | 168 | runtime_parameters = {} 169 | for parameter_name, parameter_value in settings.items(): 170 | runtime_parameters[parameter_name] = parameter_value.get("value") 171 | 172 | tool_entity = ToolEntity( 173 | identity=identity, 174 | parameters=parameters, 175 | description=description, 176 | provider_type=provider_type, 177 | runtime_parameters=runtime_parameters, 178 | ) 179 | 180 | result.append(tool_entity) 181 | 182 | return result 183 | 184 | def _init_mcp_tools(self, tools: list[ToolEntity] | None) -> list[dict]: 185 | """ 186 | Init mcp tools 187 | """ 188 | 189 | mcp_tools = [] 190 | for tool in tools or []: 191 | try: 192 | mcp_tool = self._convert_tool_to_mcp_tool(tool) 193 | except Exception: 194 | logging.exception("Failed to convert Dify tool to MCP tool") 195 | continue 196 | 197 | mcp_tools.append(mcp_tool) 198 | 199 | return mcp_tools 200 | 201 | def _convert_tool_to_mcp_tool(self, tool: ToolEntity) -> dict: 202 | """ 203 | convert tool to prompt message tool 204 | """ 205 | mcp_tool = { 206 | "name": tool.identity.name, 207 | "description": tool.description.llm if tool.description else "", 208 | "inputSchema": { 209 | "type": "object", 210 | "properties": {}, 211 | "required": [] 212 | } 213 | } 214 | 215 | parameters = tool.parameters 216 | for parameter in parameters: 217 | if parameter.form != ToolParameter.ToolParameterForm.LLM: 218 | continue 219 | 220 | parameter_type = parameter.type 221 | if parameter.type in { 222 | ToolParameter.ToolParameterType.FILE, 223 | ToolParameter.ToolParameterType.FILES, 224 | }: 225 | continue 226 | enum = [] 227 | if parameter.type == ToolParameter.ToolParameterType.SELECT: 228 | enum = [option.value for option in parameter.options] if parameter.options else [] 229 | 230 | mcp_tool["inputSchema"]["properties"][parameter.name] = { 231 | "type": parameter_type, 232 | "description": parameter.llm_description or "", 233 | } 234 | 235 | if len(enum) > 0: 236 | mcp_tool["inputSchema"]["properties"][parameter.name]["enum"] = enum 237 | 238 | if parameter.required: 239 | mcp_tool["inputSchema"]["required"].append(parameter.name) 240 | 241 | return mcp_tool 242 | 243 | def _invoke_tool(self, tool: ToolEntity, tool_call_args) -> str: 244 | """ 245 | invoke tool 246 | """ 247 | 248 | tool_invoke_responses = self.session.tool.invoke( 249 | provider_type=ToolProviderType(tool.provider_type), 250 | provider=tool.identity.provider, 251 | tool_name=tool.identity.name, 252 | parameters={**tool.runtime_parameters, **tool_call_args}, 253 | ) 254 | 255 | result = "" 256 | for response in tool_invoke_responses: 257 | if response.type == ToolInvokeMessage.MessageType.TEXT: 258 | result += cast(ToolInvokeMessage.TextMessage, response.message).text 259 | elif response.type == ToolInvokeMessage.MessageType.LINK: 260 | result += ( 261 | f"result link: {cast(ToolInvokeMessage.TextMessage, response.message).text}." 262 | + " please tell user to check it." 263 | ) 264 | elif response.type in { 265 | ToolInvokeMessage.MessageType.IMAGE_LINK, 266 | ToolInvokeMessage.MessageType.IMAGE, 267 | }: 268 | result += f"Not support message type: {response.type}." 269 | elif response.type == ToolInvokeMessage.MessageType.JSON: 270 | text = json.dumps( 271 | cast(ToolInvokeMessage.JsonMessage, response.message).json_object, 272 | ensure_ascii=False, 273 | ) 274 | result += f"tool response: {text}." 275 | else: 276 | result += f"tool response: {response.message!r}." 277 | 278 | return result 279 | -------------------------------------------------------------------------------- /endpoints/messages.yaml: -------------------------------------------------------------------------------- 1 | path: "/messages/" 2 | method: "POST" 3 | extra: 4 | python: 5 | source: "endpoints/messages.py" 6 | -------------------------------------------------------------------------------- /endpoints/sse.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import uuid 4 | from typing import Mapping 5 | 6 | from dify_plugin import Endpoint 7 | from werkzeug import Request, Response 8 | 9 | 10 | def create_sse_message(event, data): 11 | return f"event: {event}\ndata: {json.dumps(data) if isinstance(data, (dict, list)) else data}\n\n" 12 | 13 | 14 | class SSEEndpoint(Endpoint): 15 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 16 | """ 17 | Invokes the endpoint with the given request. 18 | """ 19 | session_id = str(uuid.uuid4()).replace("-", "") 20 | 21 | def generate(): 22 | endpoint = f"messages/?session_id={session_id}" 23 | yield create_sse_message("endpoint", endpoint) 24 | 25 | while True: 26 | if self.session.storage.exist(session_id): 27 | message = self.session.storage.get(session_id) 28 | message = message.decode() 29 | self.session.storage.delete(session_id) 30 | yield create_sse_message("message", message) 31 | time.sleep(0.5) 32 | 33 | return Response(generate(), status=200, content_type="text/event-stream") 34 | -------------------------------------------------------------------------------- /endpoints/sse.yaml: -------------------------------------------------------------------------------- 1 | path: "/sse" 2 | method: "GET" 3 | extra: 4 | python: 5 | source: "endpoints/sse.py" 6 | -------------------------------------------------------------------------------- /group/mcp_compat_dify_tools.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | - name: tools 3 | type: array[tools] 4 | required: true 5 | label: 6 | en_US: Tool list 7 | zh_Hans: 工具列表 8 | endpoints: 9 | - endpoints/sse.yaml 10 | - endpoints/messages.yaml 11 | - endpoints/mcp_get.yaml 12 | - endpoints/mcp_post.yaml -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from dify_plugin import Plugin, DifyPluginEnv 2 | 3 | plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120)) 4 | 5 | if __name__ == '__main__': 6 | plugin.run() 7 | -------------------------------------------------------------------------------- /manifest.yaml: -------------------------------------------------------------------------------- 1 | version: 0.1.1 2 | type: plugin 3 | author: junjiem 4 | name: mcp_compat_dify_tools 5 | label: 6 | en_US: MCP Compatible Dify Tools 7 | zh_Hans: MCP Compatible Dify Tools 8 | description: 9 | en_US: "Convert your Dify tools's API to MCP compatible API (Note: must dify 1.2.0+)" 10 | zh_Hans: 将您的Dify工具的API转换为MCP兼容API(注:必须 dify 1.2.0+) 11 | icon: icon.svg 12 | resource: 13 | memory: 268435456 14 | permission: 15 | endpoint: 16 | enabled: true 17 | tool: 18 | enabled: true 19 | storage: 20 | enabled: true 21 | size: 1048576 22 | plugins: 23 | endpoints: 24 | - group/mcp_compat_dify_tools.yaml 25 | meta: 26 | version: 0.0.1 27 | arch: 28 | - amd64 29 | - arm64 30 | runner: 31 | language: python 32 | version: "3.12" 33 | entrypoint: main 34 | created_at: 2025-04-11T15:35:09.8828922+08:00 35 | privacy: PRIVACY.md 36 | verified: false 37 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dify_plugin>=0.2.1,<0.3.0 --------------------------------------------------------------------------------