#
tokens: 12669/50000 18/18 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```