# Directory Structure
```
├── _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
├── .difyignore
├── .env.example
├── .github
│ └── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── config.yml
│ └── feature_request.yml
├── .gitignore
├── 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
├── GUIDE.md
├── main.py
├── manifest.yaml
├── PRIVACY.md
├── README.md
└── requirements.txt
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
INSTALL_METHOD=remote
REMOTE_INSTALL_HOST=debug.dify.ai
REMOTE_INSTALL_PORT=5003
REMOTE_INSTALL_KEY=********-****-****-****-************
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# Vscode
.vscode/
```
--------------------------------------------------------------------------------
/.difyignore:
--------------------------------------------------------------------------------
```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# Vscode
.vscode/
# Git
.git/
.gitignore
# Mac
.DS_Store
# Windows
Thumbs.db
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
dify_plugin>=0.2.1,<0.3.0
```
--------------------------------------------------------------------------------
/endpoints/sse.yaml:
--------------------------------------------------------------------------------
```yaml
path: "/sse"
method: "GET"
extra:
python:
source: "endpoints/sse.py"
```
--------------------------------------------------------------------------------
/endpoints/mcp_get.yaml:
--------------------------------------------------------------------------------
```yaml
path: "/mcp"
method: "GET"
extra:
python:
source: "endpoints/mcp_get.py"
```
--------------------------------------------------------------------------------
/endpoints/mcp_post.yaml:
--------------------------------------------------------------------------------
```yaml
path: "/mcp"
method: "POST"
extra:
python:
source: "endpoints/mcp_post.py"
```
--------------------------------------------------------------------------------
/endpoints/messages.yaml:
--------------------------------------------------------------------------------
```yaml
path: "/messages/"
method: "POST"
extra:
python:
source: "endpoints/messages.py"
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
from dify_plugin import Plugin, DifyPluginEnv
plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120))
if __name__ == '__main__':
plugin.run()
```
--------------------------------------------------------------------------------
/group/mcp_compat_dify_tools.yaml:
--------------------------------------------------------------------------------
```yaml
settings:
- name: tools
type: array[tools]
required: true
label:
en_US: Tool list
zh_Hans: 工具列表
endpoints:
- endpoints/sse.yaml
- endpoints/messages.yaml
- endpoints/mcp_get.yaml
- endpoints/mcp_post.yaml
```
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
```markdown
# Privacy Policy
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.
## Data Collection
- **No Personal Information**: We do not collect, store, or process any personal information.
- **No Usage Data**: We do not track or monitor how you use the tool.
- **No Analytics**: We do not implement any analytics or tracking mechanisms.
## Third-Party Services
This tool does not integrate with or utilize any third-party services that might collect user data.
## Changes to Privacy Policy
If there are any changes to our privacy practices, we will update this document accordingly.
```
--------------------------------------------------------------------------------
/endpoints/mcp_get.py:
--------------------------------------------------------------------------------
```python
from typing import Mapping
from dify_plugin import Endpoint
from werkzeug import Request, Response
class McpGetEndpoint(Endpoint):
def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response:
"""
Streamable HTTP in Dify is a lightweight design,
it only supported POST and don't support Server-Sent Events (SSE).
"""
response = {
"jsonrpc": "2.0",
"id": None,
"error": {
"code": -32000,
"message": "Not support make use of Server-Sent Events (SSE) to stream multiple server messages."
},
}
return Response(response, status=405, content_type="application/json")
```
--------------------------------------------------------------------------------
/manifest.yaml:
--------------------------------------------------------------------------------
```yaml
version: 0.1.1
type: plugin
author: junjiem
name: mcp_compat_dify_tools
label:
en_US: MCP Compatible Dify Tools
zh_Hans: MCP Compatible Dify Tools
description:
en_US: "Convert your Dify tools's API to MCP compatible API (Note: must dify 1.2.0+)"
zh_Hans: 将您的Dify工具的API转换为MCP兼容API(注:必须 dify 1.2.0+)
icon: icon.svg
resource:
memory: 268435456
permission:
endpoint:
enabled: true
tool:
enabled: true
storage:
enabled: true
size: 1048576
plugins:
endpoints:
- group/mcp_compat_dify_tools.yaml
meta:
version: 0.0.1
arch:
- amd64
- arm64
runner:
language: python
version: "3.12"
entrypoint: main
created_at: 2025-04-11T15:35:09.8828922+08:00
privacy: PRIVACY.md
verified: false
```
--------------------------------------------------------------------------------
/endpoints/sse.py:
--------------------------------------------------------------------------------
```python
import json
import time
import uuid
from typing import Mapping
from dify_plugin import Endpoint
from werkzeug import Request, Response
def create_sse_message(event, data):
return f"event: {event}\ndata: {json.dumps(data) if isinstance(data, (dict, list)) else data}\n\n"
class SSEEndpoint(Endpoint):
def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response:
"""
Invokes the endpoint with the given request.
"""
session_id = str(uuid.uuid4()).replace("-", "")
def generate():
endpoint = f"messages/?session_id={session_id}"
yield create_sse_message("endpoint", endpoint)
while True:
if self.session.storage.exist(session_id):
message = self.session.storage.get(session_id)
message = message.decode()
self.session.storage.delete(session_id)
yield create_sse_message("message", message)
time.sleep(0.5)
return Response(generate(), status=200, content_type="text/event-stream")
```
--------------------------------------------------------------------------------
/_assets/icon.svg:
--------------------------------------------------------------------------------
```
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1744362742132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8998"
width="48" height="48">
<path d="M171.84 477.184l296.192-296.128q39.808-39.808 96.064-39.808 56.32 0 96.128 39.808t39.808 96.064q0 7.488-0.704 14.72 6.848-0.64 13.888-0.64 57.152 0 97.536 40.32 40.32 40.448 40.32 97.536t-40.32 97.472l-269.696 269.696q-4.928 4.928 0 9.856l57.152 57.152a30.464 30.464 0 0 1-43.072 43.072l-57.152-57.152q-19.84-19.84-19.84-48t19.84-48l269.696-269.696q22.528-22.528 22.528-54.4t-22.528-54.4q-22.592-22.592-54.4-22.592-31.936 0-54.464 22.592l-17.28 17.216L435.2 598.336a30.464 30.464 0 0 1-43.008-0.064l-0.192-0.128a30.272 30.272 0 0 1 0.128-42.88L599.68 347.52l17.408-17.408q21.952-22.016 21.952-53.056t-21.952-52.992q-22.016-22.016-53.056-22.016t-52.992 22.016L214.912 520.256a30.464 30.464 0 0 1-43.072-43.072z m567.104-29.44L513.92 672.96q-39.808 39.808-96.128 39.808t-96.128-39.808q-39.808-39.808-39.808-96.128t39.808-96.128l225.088-225.024a30.464 30.464 0 0 1 43.008 43.008L364.8 523.712q-21.952 22.016-21.952 53.056t21.952 53.056q22.016 21.952 53.056 21.952t52.992-21.952l225.088-225.088a30.464 30.464 0 0 1 43.072 43.072z"
p-id="8999" fill="#1296db"></path>
</svg>
```
--------------------------------------------------------------------------------
/GUIDE.md:
--------------------------------------------------------------------------------
```markdown
## User Guide of how to develop a Dify Plugin
Hi there, looks like you have already created a Plugin, now let's get you started with the development!
### Choose a Plugin type you want to develop
Before start, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify:
- **Tool**: Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task.
- **Model**: Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities.
- **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.
Based on the ability you want to extend, we have divided the Plugin into three types: **Tool**, **Model**, and **Extension**.
- **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.
- **Model**: Just a model provider, extending others is not allowed.
- **Extension**: Other times, you may only need a simple http service to extend the functionalities, **Extension** is the right choice for you.
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.
### Manifest
Now you can edit the `manifest.yaml` file to describe your Plugin, here is the basic structure of it:
- version(version, required):Plugin's version
- type(type, required):Plugin's type, currently only supports `plugin`, future support `bundle`
- author(string, required):Author, it's the organization name in Marketplace and should also equals to the owner of the repository
- label(label, required):Multi-language name
- created_at(RFC3339, required):Creation time, Marketplace requires that the creation time must be less than the current time
- icon(asset, required):Icon path
- resource (object):Resources to be applied
- memory (int64):Maximum memory usage, mainly related to resource application on SaaS for serverless, unit bytes
- permission(object):Permission application
- tool(object):Reverse call tool permission
- enabled (bool)
- model(object):Reverse call model permission
- enabled(bool)
- llm(bool)
- text_embedding(bool)
- rerank(bool)
- tts(bool)
- speech2text(bool)
- moderation(bool)
- node(object):Reverse call node permission
- enabled(bool)
- endpoint(object):Allow to register endpoint permission
- enabled(bool)
- app(object):Reverse call app permission
- enabled(bool)
- storage(object):Apply for persistent storage permission
- enabled(bool)
- size(int64):Maximum allowed persistent memory, unit bytes
- 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.
- Format
- tools(list[string]): Extended tool suppliers, as for the detailed format, please refer to [Tool Guide](https://docs.dify.ai/plugins/schema-definition/tool)
- models(list[string]):Extended model suppliers, as for the detailed format, please refer to [Model Guide](https://docs.dify.ai/plugins/schema-definition/model)
- endpoints(list[string]):Extended Endpoints suppliers, as for the detailed format, please refer to [Endpoint Guide](https://docs.dify.ai/plugins/schema-definition/endpoint)
- Restrictions
- Not allowed to extend both tools and models
- Not allowed to have no extension
- Not allowed to extend both models and endpoints
- Currently only supports up to one supplier of each type of extension
- meta(object)
- version(version, required):manifest format version, initial version 0.0.1
- arch(list[string], required):Supported architectures, currently only supports amd64 arm64
- runner(object, required):Runtime configuration
- language(string):Currently only supports python
- version(string):Language version, currently only supports 3.12
- entrypoint(string):Program entry, in python it should be main
### Install Dependencies
- First of all, you need a Python 3.11+ environment, as our SDK requires that.
- Then, install the dependencies:
```bash
pip install -r requirements.txt
```
- 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.
### Implement the Plugin
Now you can start to implement your Plugin, by following these examples, you can quickly understand how to implement your own Plugin:
- [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai): best practice for model provider
- [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google): a simple example for tool provider
- [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko): a funny example for endpoint group
### Test and Debug the Plugin
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.
- `INSTALL_METHOD`: Set this to `remote`, your plugin will connect to a Dify instance through the network.
- `REMOTE_INSTALL_HOST`: The host of your Dify instance, you can use our SaaS instance `https://debug.dify.ai`, or self-hosted Dify instance.
- `REMOTE_INSTALL_PORT`: The port of your Dify instance, default is 5003
- `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.
Run the following command to start your Plugin:
```bash
python -m main
```
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.
### Package the Plugin
After all, just package your Plugin by running the following command:
```bash
dify-plugin plugin package ./ROOT_DIRECTORY_OF_YOUR_PLUGIN
```
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!
## User Privacy Policy
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.
```
--------------------------------------------------------------------------------
/endpoints/messages.py:
--------------------------------------------------------------------------------
```python
import json
import logging
from typing import Mapping, cast, Any
from dify_plugin import Endpoint
from dify_plugin.entities import I18nObject
from dify_plugin.entities.tool import ToolParameter, ToolProviderType, ToolInvokeMessage, ToolDescription
from dify_plugin.interfaces.agent import ToolEntity, AgentToolIdentity
from pydantic import BaseModel
from werkzeug import Request, Response
class EndpointParams(BaseModel):
tools: list[ToolEntity] | None
class MessageEndpoint(Endpoint):
def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response:
"""
Invokes the endpoint with the given request.
"""
session_id = r.args.get('session_id')
data = r.json
method = data.get("method")
print("===============tools==============")
print(settings.get("tools"))
if method == "initialize":
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"experimental": {},
"prompts": {"listChanged": False},
"resources": {
"subscribe": False,
"listChanged": False
},
"tools": {"listChanged": False}
},
"serverInfo": {
"name": "MCP Compatible Dify Tools",
"version": "1.0.0"
}
}
}
elif method == "notifications/initialized":
return Response("", status=202, content_type="application/json")
elif method == "tools/list":
try:
tools: list[ToolEntity] = self._init_tools(settings.get("tools"))
mcp_tools = self._init_mcp_tools(tools)
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"result": {
"tools": mcp_tools
}
}
except Exception as e:
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"error": {
"code": -32000,
"message": str(e)
}
}
elif method == "tools/call":
try:
tools: list[ToolEntity] = self._init_tools(settings.get("tools"))
tool_instances = {tool.identity.name: tool for tool in tools} if tools else {}
tool_name = data.get("params", {}).get("name")
arguments = data.get("params", {}).get("arguments", {})
tool_instance = tool_instances.get(tool_name)
if tool_instance:
result = self._invoke_tool(tool_instance, arguments)
else:
raise ValueError(f"Unknown tool: {tool_name}")
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"result": {
"content": [{"type": "text", "text": result}],
"isError": False
}
}
except Exception as e:
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"error": {
"code": -32000,
"message": str(e)
}
}
else:
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"error": {
"code": -32001,
"message": f"Unsupported method: {method}"
}
}
self.session.storage.set(session_id, json.dumps(response).encode())
return Response("", status=202, content_type="application/json")
def _init_tools(self, tools_param_value) -> list[ToolEntity]:
"""
init ToolEntity list
"""
result: list[ToolEntity] = []
value = cast(list[dict[str, Any]], tools_param_value)
value = [tool for tool in value if tool.get("enabled", False)]
for tool in value:
tool_type = tool["type"]
tool_name = tool["tool_name"]
tool_label = tool["tool_label"]
tool_description = tool.get("tool_description", None)
extra_description = tool.get("extra", {}).get("description", None)
provider_name = tool["provider_name"]
schemas = tool.get("schemas", [])
settings = tool.get("settings", {})
identity = AgentToolIdentity(
author="Dify",
name=tool_name,
label=I18nObject(en_US=tool_label),
provider=provider_name,
)
llm_description = (
extra_description
if extra_description else tool_description
if tool_description else tool_label
)
description = ToolDescription(
human=I18nObject(en_US=llm_description),
llm=llm_description,
)
provider_type = ToolProviderType.BUILT_IN
if tool_type == "api":
provider_type = ToolProviderType.API
elif tool_type == "workflow":
provider_type = ToolProviderType.WORKFLOW
parameters = []
for schema in schemas:
parameters.append(ToolParameter(**schema))
runtime_parameters = {}
for parameter_name, parameter_value in settings.items():
runtime_parameters[parameter_name] = parameter_value.get("value")
tool_entity = ToolEntity(
identity=identity,
parameters=parameters,
description=description,
provider_type=provider_type,
runtime_parameters=runtime_parameters,
)
result.append(tool_entity)
return result
def _init_mcp_tools(self, tools: list[ToolEntity] | None) -> list[dict]:
"""
Init mcp tools
"""
mcp_tools = []
for tool in tools or []:
try:
mcp_tool = self._convert_tool_to_mcp_tool(tool)
except Exception:
logging.exception("Failed to convert Dify tool to MCP tool")
continue
mcp_tools.append(mcp_tool)
return mcp_tools
def _convert_tool_to_mcp_tool(self, tool: ToolEntity) -> dict:
"""
convert tool to prompt message tool
"""
mcp_tool = {
"name": tool.identity.name,
"description": tool.description.llm if tool.description else "",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
}
parameters = tool.parameters
for parameter in parameters:
if parameter.form != ToolParameter.ToolParameterForm.LLM:
continue
parameter_type = parameter.type
if parameter.type in {
ToolParameter.ToolParameterType.FILE,
ToolParameter.ToolParameterType.FILES,
}:
continue
enum = []
if parameter.type == ToolParameter.ToolParameterType.SELECT:
enum = [option.value for option in parameter.options] if parameter.options else []
mcp_tool["inputSchema"]["properties"][parameter.name] = {
"type": parameter_type,
"description": parameter.llm_description or "",
}
if len(enum) > 0:
mcp_tool["inputSchema"]["properties"][parameter.name]["enum"] = enum
if parameter.required:
mcp_tool["inputSchema"]["required"].append(parameter.name)
return mcp_tool
def _invoke_tool(self, tool: ToolEntity, tool_call_args) -> str:
"""
invoke tool
"""
tool_invoke_responses = self.session.tool.invoke(
provider_type=ToolProviderType(tool.provider_type),
provider=tool.identity.provider,
tool_name=tool.identity.name,
parameters={**tool.runtime_parameters, **tool_call_args},
)
result = ""
for response in tool_invoke_responses:
if response.type == ToolInvokeMessage.MessageType.TEXT:
result += cast(ToolInvokeMessage.TextMessage, response.message).text
elif response.type == ToolInvokeMessage.MessageType.LINK:
result += (
f"result link: {cast(ToolInvokeMessage.TextMessage, response.message).text}."
+ " please tell user to check it."
)
elif response.type in {
ToolInvokeMessage.MessageType.IMAGE_LINK,
ToolInvokeMessage.MessageType.IMAGE,
}:
result += f"Not support message type: {response.type}."
elif response.type == ToolInvokeMessage.MessageType.JSON:
text = json.dumps(
cast(ToolInvokeMessage.JsonMessage, response.message).json_object,
ensure_ascii=False,
)
result += f"tool response: {text}."
else:
result += f"tool response: {response.message!r}."
return result
```
--------------------------------------------------------------------------------
/endpoints/mcp_post.py:
--------------------------------------------------------------------------------
```python
import json
import logging
import uuid
from typing import Mapping, cast, Any
from dify_plugin import Endpoint
from dify_plugin.entities import I18nObject
from dify_plugin.entities.tool import ToolParameter, ToolProviderType, ToolInvokeMessage, ToolDescription
from dify_plugin.interfaces.agent import ToolEntity, AgentToolIdentity
from pydantic import BaseModel
from werkzeug import Request, Response
class EndpointParams(BaseModel):
tools: list[ToolEntity] | None
class McpPostEndpoint(Endpoint):
def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response:
"""
The simplest MCP Streamable HTTP transport implementation.
1. not validate the `Origin` header
2. not authentication
3. not valid session id
4. not support Server-Sent Events (SSE)
"""
session_id = r.args.get('session_id')
data = r.json
method = data.get("method")
print("===============tools==============")
print(settings.get("tools"))
if method == "initialize":
session_id = str(uuid.uuid4()).replace("-", "")
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
},
"serverInfo": {
"name": "MCP Compatible Dify Tools",
"version": "1.0.0"
},
},
}
headers = {"mcp-session-id": session_id}
return Response(
json.dumps(response),
status=200,
content_type="application/json",
headers=headers,
)
elif method == "notifications/initialized":
return Response("", status=202, content_type="application/json")
elif method == "tools/list":
try:
tools: list[ToolEntity] = self._init_tools(settings.get("tools"))
mcp_tools = self._init_mcp_tools(tools)
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"result": {
"tools": mcp_tools
}
}
except Exception as e:
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"error": {
"code": -32000,
"message": str(e)
}
}
elif method == "tools/call":
try:
tools: list[ToolEntity] = self._init_tools(settings.get("tools"))
tool_instances = {tool.identity.name: tool for tool in tools} if tools else {}
tool_name = data.get("params", {}).get("name")
arguments = data.get("params", {}).get("arguments", {})
tool_instance = tool_instances.get(tool_name)
if tool_instance:
result = self._invoke_tool(tool_instance, arguments)
else:
raise ValueError(f"Unknown tool: {tool_name}")
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"result": {
"content": [{"type": "text", "text": result}],
"isError": False
}
}
except Exception as e:
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"error": {
"code": -32000,
"message": str(e)
}
}
else:
response = {
"jsonrpc": "2.0",
"id": data.get("id"),
"error": {
"code": -32001,
"message": f"Unsupported method: {method}"
}
}
return Response(
json.dumps(response), status=200, content_type="application/json"
)
def _init_tools(self, tools_param_value) -> list[ToolEntity]:
"""
init ToolEntity list
"""
result: list[ToolEntity] = []
value = cast(list[dict[str, Any]], tools_param_value)
value = [tool for tool in value if tool.get("enabled", False)]
for tool in value:
tool_type = tool["type"]
tool_name = tool["tool_name"]
tool_label = tool["tool_label"]
tool_description = tool.get("tool_description", None)
extra_description = tool.get("extra", {}).get("description", None)
provider_name = tool["provider_name"]
schemas = tool.get("schemas", [])
settings = tool.get("settings", {})
identity = AgentToolIdentity(
author="Dify",
name=tool_name,
label=I18nObject(en_US=tool_label),
provider=provider_name,
)
llm_description = (
extra_description
if extra_description else tool_description
if tool_description else tool_label
)
description = ToolDescription(
human=I18nObject(en_US=llm_description),
llm=llm_description,
)
provider_type = ToolProviderType.BUILT_IN
if tool_type == "api":
provider_type = ToolProviderType.API
elif tool_type == "workflow":
provider_type = ToolProviderType.WORKFLOW
parameters = []
for schema in schemas:
parameters.append(ToolParameter(**schema))
runtime_parameters = {}
for parameter_name, parameter_value in settings.items():
runtime_parameters[parameter_name] = parameter_value.get("value")
tool_entity = ToolEntity(
identity=identity,
parameters=parameters,
description=description,
provider_type=provider_type,
runtime_parameters=runtime_parameters,
)
result.append(tool_entity)
return result
def _init_mcp_tools(self, tools: list[ToolEntity] | None) -> list[dict]:
"""
Init mcp tools
"""
mcp_tools = []
for tool in tools or []:
try:
mcp_tool = self._convert_tool_to_mcp_tool(tool)
except Exception:
logging.exception("Failed to convert Dify tool to MCP tool")
continue
mcp_tools.append(mcp_tool)
return mcp_tools
def _convert_tool_to_mcp_tool(self, tool: ToolEntity) -> dict:
"""
convert tool to prompt message tool
"""
mcp_tool = {
"name": tool.identity.name,
"description": tool.description.llm if tool.description else "",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
}
parameters = tool.parameters
for parameter in parameters:
if parameter.form != ToolParameter.ToolParameterForm.LLM:
continue
parameter_type = parameter.type
if parameter.type in {
ToolParameter.ToolParameterType.FILE,
ToolParameter.ToolParameterType.FILES,
}:
continue
enum = []
if parameter.type == ToolParameter.ToolParameterType.SELECT:
enum = [option.value for option in parameter.options] if parameter.options else []
mcp_tool["inputSchema"]["properties"][parameter.name] = {
"type": parameter_type,
"description": parameter.llm_description or "",
}
if len(enum) > 0:
mcp_tool["inputSchema"]["properties"][parameter.name]["enum"] = enum
if parameter.required:
mcp_tool["inputSchema"]["required"].append(parameter.name)
return mcp_tool
def _invoke_tool(self, tool: ToolEntity, tool_call_args) -> str:
"""
invoke tool
"""
tool_invoke_responses = self.session.tool.invoke(
provider_type=ToolProviderType(tool.provider_type),
provider=tool.identity.provider,
tool_name=tool.identity.name,
parameters={**tool.runtime_parameters, **tool_call_args},
)
result = ""
for response in tool_invoke_responses:
if response.type == ToolInvokeMessage.MessageType.TEXT:
result += cast(ToolInvokeMessage.TextMessage, response.message).text
elif response.type == ToolInvokeMessage.MessageType.LINK:
result += (
f"result link: {cast(ToolInvokeMessage.TextMessage, response.message).text}."
+ " please tell user to check it."
)
elif response.type in {
ToolInvokeMessage.MessageType.IMAGE_LINK,
ToolInvokeMessage.MessageType.IMAGE,
}:
result += f"Not support message type: {response.type}."
elif response.type == ToolInvokeMessage.MessageType.JSON:
text = json.dumps(
cast(ToolInvokeMessage.JsonMessage, response.message).json_object,
ensure_ascii=False,
)
result += f"tool response: {text}."
else:
result += f"tool response: {response.message!r}."
return result
```