# 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:
--------------------------------------------------------------------------------
```
1 | INSTALL_METHOD=remote
2 | REMOTE_INSTALL_HOST=debug.dify.ai
3 | REMOTE_INSTALL_PORT=5003
4 | REMOTE_INSTALL_KEY=********-****-****-****-************
5 |
```
--------------------------------------------------------------------------------
/.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 |
```
--------------------------------------------------------------------------------
/.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 |
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | dify_plugin>=0.2.1,<0.3.0
```
--------------------------------------------------------------------------------
/endpoints/sse.yaml:
--------------------------------------------------------------------------------
```yaml
1 | path: "/sse"
2 | method: "GET"
3 | extra:
4 | python:
5 | source: "endpoints/sse.py"
6 |
```
--------------------------------------------------------------------------------
/endpoints/mcp_get.yaml:
--------------------------------------------------------------------------------
```yaml
1 | path: "/mcp"
2 | method: "GET"
3 | extra:
4 | python:
5 | source: "endpoints/mcp_get.py"
6 |
```
--------------------------------------------------------------------------------
/endpoints/mcp_post.yaml:
--------------------------------------------------------------------------------
```yaml
1 | path: "/mcp"
2 | method: "POST"
3 | extra:
4 | python:
5 | source: "endpoints/mcp_post.py"
6 |
```
--------------------------------------------------------------------------------
/endpoints/messages.yaml:
--------------------------------------------------------------------------------
```yaml
1 | path: "/messages/"
2 | method: "POST"
3 | extra:
4 | python:
5 | source: "endpoints/messages.py"
6 |
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
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 |
```
--------------------------------------------------------------------------------
/group/mcp_compat_dify_tools.yaml:
--------------------------------------------------------------------------------
```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
```
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
```markdown
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 |
```
--------------------------------------------------------------------------------
/endpoints/mcp_get.py:
--------------------------------------------------------------------------------
```python
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 |
```
--------------------------------------------------------------------------------
/manifest.yaml:
--------------------------------------------------------------------------------
```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 |
```
--------------------------------------------------------------------------------
/endpoints/sse.py:
--------------------------------------------------------------------------------
```python
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 |
```
--------------------------------------------------------------------------------
/_assets/icon.svg:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
2 | "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 | <svg t="1744362742132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8998"
4 | width="48" height="48">
5 | <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"
6 | p-id="8999" fill="#1296db"></path>
7 | </svg>
```
--------------------------------------------------------------------------------
/GUIDE.md:
--------------------------------------------------------------------------------
```markdown
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.
```
--------------------------------------------------------------------------------
/endpoints/messages.py:
--------------------------------------------------------------------------------
```python
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/mcp_post.py:
--------------------------------------------------------------------------------
```python
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 |
```