├── .difyignore ├── .env.example ├── .github └── workflows │ └── plugin-publish.yml ├── .gitignore ├── GUIDE.md ├── PRIVACY.md ├── README.md ├── _assets ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png └── icon.svg ├── endpoints ├── http_get.py ├── http_get.yaml ├── http_post.py ├── http_post.yaml ├── messages.py ├── messages.yaml ├── sse.py └── sse.yaml ├── group └── mcp-server.yaml ├── main.py ├── manifest.yaml └── requirements.txt /.difyignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | share/python-wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | cover/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | db.sqlite3-journal 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | .pybuilder/ 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | # For a library or package, you might want to ignore these files since the code is 84 | # intended to run in multiple environments; otherwise, check them in: 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | Pipfile.lock 93 | 94 | # UV 95 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 96 | # This is especially recommended for binary packages to ensure reproducibility, and is more 97 | # commonly ignored for libraries. 98 | uv.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | .idea/ 166 | 167 | # Vscode 168 | .vscode/ 169 | 170 | # Git 171 | .git/ 172 | .gitignore 173 | 174 | # Mac 175 | .DS_Store 176 | 177 | # Windows 178 | Thumbs.db 179 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INSTALL_METHOD=remote 2 | REMOTE_INSTALL_HOST=debug.dify.ai 3 | REMOTE_INSTALL_PORT=5003 4 | REMOTE_INSTALL_KEY=********-****-****-****-************ 5 | -------------------------------------------------------------------------------- /.github/workflows/plugin-publish.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/auto-pr.yml 2 | name: Auto Create PR on Main Push 3 | 4 | on: 5 | push: 6 | branches: [ main ] # Trigger on push to main 7 | 8 | jobs: 9 | create_pr: # Renamed job for clarity 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Print working directory # Kept for debugging 16 | run: | 17 | pwd 18 | ls -la 19 | 20 | - name: Download CLI tool 21 | run: | 22 | # Create bin directory in runner temp 23 | mkdir -p $RUNNER_TEMP/bin 24 | cd $RUNNER_TEMP/bin 25 | 26 | # Download CLI tool 27 | wget https://github.com/langgenius/dify-plugin-daemon/releases/download/0.0.6/dify-plugin-linux-amd64 28 | chmod +x dify-plugin-linux-amd64 29 | 30 | # Show download location and file 31 | echo "CLI tool location:" 32 | pwd 33 | ls -la dify-plugin-linux-amd64 34 | 35 | - name: Get basic info from manifest # Changed step name and content 36 | id: get_basic_info 37 | run: | 38 | PLUGIN_NAME=$(grep "^name:" manifest.yaml | cut -d' ' -f2) 39 | echo "Plugin name: $PLUGIN_NAME" 40 | echo "plugin_name=$PLUGIN_NAME" >> $GITHUB_OUTPUT 41 | 42 | VERSION=$(grep "^version:" manifest.yaml | cut -d' ' -f2) 43 | echo "Plugin version: $VERSION" 44 | echo "version=$VERSION" >> $GITHUB_OUTPUT 45 | 46 | # If the author's name is not your github username, you can change the author here 47 | AUTHOR=$(grep "^author:" manifest.yaml | cut -d' ' -f2) 48 | echo "Plugin author: $AUTHOR" 49 | echo "author=$AUTHOR" >> $GITHUB_OUTPUT 50 | 51 | - name: Package Plugin 52 | id: package 53 | run: | 54 | # Use the downloaded CLI tool to package 55 | cd $GITHUB_WORKSPACE 56 | # Use variables for package name 57 | PACKAGE_NAME="${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}.difypkg" 58 | # Use CLI from runner temp 59 | $RUNNER_TEMP/bin/dify-plugin-linux-amd64 plugin package . -o "$PACKAGE_NAME" 60 | 61 | # Show packaging result 62 | echo "Package result:" 63 | ls -la "$PACKAGE_NAME" 64 | echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT 65 | 66 | # Show full file path and directory structure (kept for debugging) 67 | echo "\\nFull file path:" 68 | pwd 69 | echo "\\nDirectory structure:" 70 | tree || ls -R 71 | 72 | - name: Checkout target repo 73 | uses: actions/checkout@v3 74 | with: 75 | # Use author variable for repository 76 | repository: ${{steps.get_basic_info.outputs.author}}/dify-plugins 77 | path: dify-plugins 78 | token: ${{ secrets.PLUGIN_ACTION }} 79 | fetch-depth: 1 # Fetch only the last commit to speed up checkout 80 | persist-credentials: true # Persist credentials for subsequent git operations 81 | 82 | - name: Prepare and create PR 83 | run: | 84 | # Debug info (kept) 85 | echo "Debug: Current directory $(pwd)" 86 | # Use variable for package name 87 | PACKAGE_NAME="${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}.difypkg" 88 | echo "Debug: Package name: $PACKAGE_NAME" 89 | ls -la 90 | 91 | # Move the packaged file to the target directory using variables 92 | mkdir -p dify-plugins/${{ steps.get_basic_info.outputs.author }}/${{ steps.get_basic_info.outputs.plugin_name }} 93 | mv "$PACKAGE_NAME" dify-plugins/${{ steps.get_basic_info.outputs.author }}/${{ steps.get_basic_info.outputs.plugin_name }}/ 94 | 95 | # Enter the target repository directory 96 | cd dify-plugins 97 | 98 | # Configure git 99 | git config user.name "GitHub Actions" 100 | git config user.email "actions@github.com" 101 | 102 | # Ensure we are on the latest main branch 103 | git fetch origin main 104 | git checkout main 105 | git pull origin main 106 | 107 | # Create and switch to a new branch using variables and new naming convention 108 | BRANCH_NAME="bump-${{ steps.get_basic_info.outputs.plugin_name }}-plugin-${{ steps.get_basic_info.outputs.version }}" 109 | git checkout -b "$BRANCH_NAME" 110 | 111 | # Add and commit changes (using git add .) 112 | git add . 113 | git status # for debugging 114 | # Use variables in commit message 115 | git commit -m "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin to version ${{ steps.get_basic_info.outputs.version }}" 116 | 117 | # Push to remote (use force just in case the branch existed before from a failed run) 118 | git push -u origin "$BRANCH_NAME" --force 119 | 120 | # Confirm branch has been pushed and wait for sync (GitHub API might need a moment) 121 | git branch -a 122 | echo "Waiting for branch to sync..." 123 | sleep 10 # Wait 10 seconds for branch sync 124 | 125 | - name: Create PR via GitHub API 126 | env: 127 | GH_TOKEN: ${{ secrets.PLUGIN_ACTION }} # Use the provided token for authentication 128 | run: | 129 | gh pr create \ 130 | --repo langgenius/dify-plugins \ 131 | --head "${{ steps.get_basic_info.outputs.author }}:${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}" \ 132 | --base main \ 133 | --title "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin to version ${{ steps.get_basic_info.outputs.version }}" \ 134 | --body "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin package to version ${{ steps.get_basic_info.outputs.version }} 135 | 136 | Changes: 137 | - Updated plugin package file" || echo "PR already exists or creation skipped." # Handle cases where PR already exists 138 | 139 | - name: Print environment info # Kept for debugging 140 | run: | 141 | echo "GITHUB_WORKSPACE: $GITHUB_WORKSPACE" 142 | echo "Current directory contents:" 143 | ls -R 144 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | .idea/ 169 | 170 | # Vscode 171 | .vscode/ 172 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | ## User Guide of how to develop a Dify Plugin 2 | 3 | Hi there, looks like you have already created a Plugin, now let's get you started with the development! 4 | 5 | ### Choose a Plugin type you want to develop 6 | 7 | Before start, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify: 8 | - **Tool**: Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task. 9 | - **Model**: Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities. 10 | - **Endpoint**: Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code. 11 | 12 | Based on the ability you want to extend, we have divided the Plugin into three types: **Tool**, **Model**, and **Extension**. 13 | 14 | - **Tool**: It's a tool provider, but not only limited to tools, you can implement an endpoint there, for example, you need both `Sending Message` and `Receiving Message` if you are building a Discord Bot, **Tool** and **Endpoint** are both required. 15 | - **Model**: Just a model provider, extending others is not allowed. 16 | - **Extension**: Other times, you may only need a simple http service to extend the functionalities, **Extension** is the right choice for you. 17 | 18 | I believe you have chosen the right type for your Plugin while creating it, if not, you can change it later by modifying the `manifest.yaml` file. 19 | 20 | ### Manifest 21 | 22 | Now you can edit the `manifest.yaml` file to describe your Plugin, here is the basic structure of it: 23 | 24 | - version(version, required):Plugin's version 25 | - type(type, required):Plugin's type, currently only supports `plugin`, future support `bundle` 26 | - author(string, required):Author, it's the organization name in Marketplace and should also equals to the owner of the repository 27 | - label(label, required):Multi-language name 28 | - created_at(RFC3339, required):Creation time, Marketplace requires that the creation time must be less than the current time 29 | - icon(asset, required):Icon path 30 | - resource (object):Resources to be applied 31 | - memory (int64):Maximum memory usage, mainly related to resource application on SaaS for serverless, unit bytes 32 | - permission(object):Permission application 33 | - tool(object):Reverse call tool permission 34 | - enabled (bool) 35 | - model(object):Reverse call model permission 36 | - enabled(bool) 37 | - llm(bool) 38 | - text_embedding(bool) 39 | - rerank(bool) 40 | - tts(bool) 41 | - speech2text(bool) 42 | - moderation(bool) 43 | - node(object):Reverse call node permission 44 | - enabled(bool) 45 | - endpoint(object):Allow to register endpoint permission 46 | - enabled(bool) 47 | - app(object):Reverse call app permission 48 | - enabled(bool) 49 | - storage(object):Apply for persistent storage permission 50 | - enabled(bool) 51 | - size(int64):Maximum allowed persistent memory, unit bytes 52 | - plugins(object, required):Plugin extension specific ability yaml file list, absolute path in the plugin package, if you need to extend the model, you need to define a file like openai.yaml, and fill in the path here, and the file on the path must exist, otherwise the packaging will fail. 53 | - Format 54 | - tools(list[string]): Extended tool suppliers, as for the detailed format, please refer to [Tool Guide](https://docs.dify.ai/docs/plugins/standard/tool_provider) 55 | - models(list[string]):Extended model suppliers, as for the detailed format, please refer to [Model Guide](https://docs.dify.ai/docs/plugins/standard/model_provider) 56 | - endpoints(list[string]):Extended Endpoints suppliers, as for the detailed format, please refer to [Endpoint Guide](https://docs.dify.ai/docs/plugins/standard/endpoint_group) 57 | - Restrictions 58 | - Not allowed to extend both tools and models 59 | - Not allowed to have no extension 60 | - Not allowed to extend both models and endpoints 61 | - Currently only supports up to one supplier of each type of extension 62 | - meta(object) 63 | - version(version, required):manifest format version, initial version 0.0.1 64 | - arch(list[string], required):Supported architectures, currently only supports amd64 arm64 65 | - runner(object, required):Runtime configuration 66 | - language(string):Currently only supports python 67 | - version(string):Language version, currently only supports 3.12 68 | - entrypoint(string):Program entry, in python it should be main 69 | 70 | ### Install Dependencies 71 | 72 | - First of all, you need a Python 3.11+ environment, as our SDK requires that. 73 | - Then, install the dependencies: 74 | ```bash 75 | pip install -r requirements.txt 76 | ``` 77 | - If you want to add more dependencies, you can add them to the `requirements.txt` file, once you have set the runner to python in the `manifest.yaml` file, `requirements.txt` will be automatically generated and used for packaging and deployment. 78 | 79 | ### Implement the Plugin 80 | 81 | Now you can start to implement your Plugin, by following these examples, you can quickly understand how to implement your own Plugin: 82 | 83 | - [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai): best practice for model provider 84 | - [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google): a simple example for tool provider 85 | - [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko): a funny example for endpoint group 86 | 87 | ### Test and Debug the Plugin 88 | 89 | You may already noticed that a `.env.example` file in the root directory of your Plugin, just copy it to `.env` and fill in the corresponding values, there are some environment variables you need to set if you want to debug your Plugin locally. 90 | 91 | - `INSTALL_METHOD`: Set this to `remote`, your plugin will connect to a Dify instance through the network. 92 | - `REMOTE_INSTALL_HOST`: The host of your Dify instance, you can use our SaaS instance `https://debug.dify.ai`, or self-hosted Dify instance. 93 | - `REMOTE_INSTALL_PORT`: The port of your Dify instance, default is 5003 94 | - `REMOTE_INSTALL_KEY`: You should get your debugging key from the Dify instance you used, at the right top of the plugin management page, you can see a button with a `debug` icon, click it and you will get the key. 95 | 96 | Run the following command to start your Plugin: 97 | 98 | ```bash 99 | python -m main 100 | ``` 101 | 102 | Refresh the page of your Dify instance, you should be able to see your Plugin in the list now, but it will be marked as `debugging`, you can use it normally, but not recommended for production. 103 | 104 | ### Package the Plugin 105 | 106 | After all, just package your Plugin by running the following command: 107 | 108 | ```bash 109 | dify-plugin plugin package ./ROOT_DIRECTORY_OF_YOUR_PLUGIN 110 | ``` 111 | 112 | you will get a `plugin.difypkg` file, that's all, you can submit it to the Marketplace now, look forward to your Plugin being listed! 113 | 114 | 115 | ## User Privacy Policy 116 | 117 | Please fill in the privacy policy of the plugin if you want to make it published on the Marketplace, refer to [PRIVACY.md](PRIVACY.md) for more details. -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | This tool is designed with privacy in mind and does not collect any user data. We are committed to maintaining your privacy and ensuring your data remains secure. 4 | 5 | ## Data Collection 6 | 7 | - **No Personal Information**: We do not collect, store, or process any personal information. 8 | - **No Usage Data**: We do not track or monitor how you use the tool. 9 | - **No Analytics**: We do not implement any analytics or tracking mechanisms. 10 | 11 | ## Third-Party Services 12 | 13 | This tool does not integrate with or utilize any third-party services that might collect user data. 14 | 15 | ## Changes to Privacy Policy 16 | 17 | If there are any changes to our privacy practices, we will update this document accordingly. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Server 2 | 3 | **Author:** hjlarry 4 | **Version:** 0.0.3 5 | **Type:** extension 6 | **Repo:** [https://github.com/hjlarry/dify-plugin-mcp_server](https://github.com/hjlarry/dify-plugin-mcp_server) 7 | **Feature Request:** [issues](https://github.com/hjlarry/dify-plugin-mcp_server/issues) 8 | 9 | 10 | A Dify endpoint plugin that change a dify app to a mcp server. 11 | 12 | **To keep your data secure, use this plugin exclusively within your private network.** 13 | 14 | ## Get Started 15 | 16 | ### 1. create a simple workflow app in dify. 17 | ![1](./_assets/1.png) 18 | 19 | ### 2. add a endpoint and select this app. 20 | ![2](./_assets/2.png) 21 | 22 | The app's input schema must define its input parameters. For a chat dify app, ensure to include a `query` field in the input schema, formatted as follows: 23 | ```json 24 | { 25 | "name": "get_weather", 26 | "description": "Get weather status for a place.", 27 | "inputSchema": { 28 | "properties": { 29 | "place": {"title": "Place", "type": "string"} 30 | }, 31 | "required": ["place"], 32 | "title": "get_weatherArguments", 33 | "type": "object" 34 | } 35 | } 36 | ``` 37 | 38 | ### 3. copy the endpoint url to your mcp client, like `Cherry Studio` 39 | 40 | #### Option 1: Use the newest Streamable HTTP protocol (Recommended) 41 | ![6](./_assets/6.png) 42 | ![7](./_assets/7.png) 43 | 44 | #### Option 2: Use the legacy SSE protocol 45 | ![3](./_assets/3.png) 46 | ![4](./_assets/4.png) 47 | 48 | ### 4. enjoy it! 49 | ![5](./_assets/5.png) 50 | 51 | 52 | ## Changelog 53 | 54 | ### 0.0.3 55 | - To fix sse get non-exist key get lots error logs on the plugin daemon. 56 | 57 | ### 0.0.2 58 | - Add a new Streamable HTTP protocol. 59 | - Update dify-plugin-sdk version. -------------------------------------------------------------------------------- /_assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjlarry/dify-plugin-mcp_server/22b71fdad2e66577aa9caade3c59356410136ad6/_assets/1.png -------------------------------------------------------------------------------- /_assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjlarry/dify-plugin-mcp_server/22b71fdad2e66577aa9caade3c59356410136ad6/_assets/2.png -------------------------------------------------------------------------------- /_assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjlarry/dify-plugin-mcp_server/22b71fdad2e66577aa9caade3c59356410136ad6/_assets/3.png -------------------------------------------------------------------------------- /_assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjlarry/dify-plugin-mcp_server/22b71fdad2e66577aa9caade3c59356410136ad6/_assets/4.png -------------------------------------------------------------------------------- /_assets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjlarry/dify-plugin-mcp_server/22b71fdad2e66577aa9caade3c59356410136ad6/_assets/5.png -------------------------------------------------------------------------------- /_assets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjlarry/dify-plugin-mcp_server/22b71fdad2e66577aa9caade3c59356410136ad6/_assets/6.png -------------------------------------------------------------------------------- /_assets/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjlarry/dify-plugin-mcp_server/22b71fdad2e66577aa9caade3c59356410136ad6/_assets/7.png -------------------------------------------------------------------------------- /_assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /endpoints/http_get.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | from werkzeug import Request, Response 4 | from dify_plugin import Endpoint 5 | 6 | 7 | class HTTPGetEndpoint(Endpoint): 8 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 9 | """ 10 | Streamable HTTP in dify is a lightweight design, it only support POST and don't support SSE. 11 | """ 12 | response = { 13 | "jsonrpc": "2.0", 14 | "id": None, 15 | "error": {"code": -32000, "message": "Method not allowed"}, 16 | } 17 | 18 | return Response(response, status=405, content_type="application/json") 19 | -------------------------------------------------------------------------------- /endpoints/http_get.yaml: -------------------------------------------------------------------------------- 1 | path: "/mcp" 2 | method: "GET" 3 | extra: 4 | python: 5 | source: "endpoints/http_get.py" -------------------------------------------------------------------------------- /endpoints/http_post.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import json 3 | from typing import Mapping 4 | 5 | from werkzeug import Request, Response 6 | from dify_plugin import Endpoint 7 | 8 | 9 | class HTTPPostEndpoint(Endpoint): 10 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 11 | """ 12 | the simplest Streamable HTTP mcp protocol implementation. 13 | 14 | 1. not valid session id 15 | 2. not support SSE 16 | 3. not support streaming 17 | 4. only basic logic 18 | """ 19 | app_id = settings.get("app").get("app_id") 20 | try: 21 | tool = json.loads(settings.get("app-input-schema")) 22 | except json.JSONDecodeError: 23 | raise ValueError("Invalid app-input-schema") 24 | 25 | session_id = r.args.get("session_id") 26 | data = r.json 27 | 28 | if data.get("method") == "initialize": 29 | session_id = str(uuid.uuid4()).replace("-", "") 30 | response = { 31 | "jsonrpc": "2.0", 32 | "id": data.get("id"), 33 | "result": { 34 | "protocolVersion": "2024-11-05", 35 | "capabilities": { 36 | "tools": {}, 37 | }, 38 | "serverInfo": {"name": "Dify", "version": "0.0.1"}, 39 | }, 40 | } 41 | headers = {"mcp-session-id": session_id} 42 | return Response( 43 | json.dumps(response), 44 | status=200, 45 | content_type="application/json", 46 | headers=headers, 47 | ) 48 | 49 | elif data.get("method") == "notifications/initialized": 50 | return Response("", status=202, content_type="application/json") 51 | 52 | elif data.get("method") == "tools/list": 53 | response = { 54 | "jsonrpc": "2.0", 55 | "id": data.get("id"), 56 | "result": {"tools": [tool]}, 57 | } 58 | 59 | elif data.get("method") == "tools/call": 60 | tool_name = data.get("params", {}).get("name") 61 | arguments = data.get("params", {}).get("arguments", {}) 62 | 63 | try: 64 | if tool_name == tool.get("name"): 65 | if settings.get("app-type") == "chat": 66 | result = self.session.app.chat.invoke( 67 | app_id=app_id, 68 | query=arguments.get("query", "empty query"), 69 | inputs=arguments, 70 | response_mode="blocking", 71 | ) 72 | else: 73 | result = self.session.app.workflow.invoke( 74 | app_id=app_id, inputs=arguments, response_mode="blocking" 75 | ) 76 | else: 77 | raise ValueError(f"Unknown tool: {tool_name}") 78 | 79 | if settings.get("app-type") == "chat": 80 | final_result = {"type": "text", "text": result.get("answer")} 81 | else: 82 | outputs = result.get("data", {}).get("outputs", {}) 83 | text_list = [] 84 | for v in outputs.values(): 85 | if isinstance(v, str): 86 | text_list.append(v) 87 | elif isinstance(v, dict) or isinstance(v, list): 88 | text_list.append(json.dumps(v, ensure_ascii=False)) 89 | else: 90 | text_list.append(str(v)) 91 | final_result = {"type": "text", "text": "\n".join(text_list)} 92 | 93 | response = { 94 | "jsonrpc": "2.0", 95 | "id": data.get("id"), 96 | "result": {"content": [final_result], "isError": False}, 97 | } 98 | except Exception as e: 99 | response = { 100 | "jsonrpc": "2.0", 101 | "id": data.get("id"), 102 | "error": {"code": -32000, "message": str(e)}, 103 | } 104 | else: 105 | response = { 106 | "jsonrpc": "2.0", 107 | "id": data.get("id"), 108 | "error": {"code": -32001, "message": "unsupported method"}, 109 | } 110 | 111 | return Response( 112 | json.dumps(response), status=200, content_type="application/json" 113 | ) 114 | -------------------------------------------------------------------------------- /endpoints/http_post.yaml: -------------------------------------------------------------------------------- 1 | path: "/mcp" 2 | method: "POST" 3 | extra: 4 | python: 5 | source: "endpoints/http_post.py" -------------------------------------------------------------------------------- /endpoints/messages.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Mapping 3 | from werkzeug import Request, Response 4 | from dify_plugin import Endpoint 5 | 6 | 7 | class MessageEndpoint(Endpoint): 8 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 9 | """ 10 | Invokes the endpoint with the given request. 11 | """ 12 | app_id = settings.get("app").get("app_id") 13 | try: 14 | tool = json.loads(settings.get("app-input-schema")) 15 | except json.JSONDecodeError: 16 | raise ValueError("Invalid app-input-schema") 17 | 18 | session_id = r.args.get("session_id") 19 | data = r.json 20 | 21 | if data.get("method") == "initialize": 22 | response = { 23 | "jsonrpc": "2.0", 24 | "id": data.get("id"), 25 | "result": { 26 | "protocolVersion": "2024-11-05", 27 | "capabilities": { 28 | "experimental": {}, 29 | "prompts": {"listChanged": False}, 30 | "resources": {"subscribe": False, "listChanged": False}, 31 | "tools": {"listChanged": False}, 32 | }, 33 | "serverInfo": {"name": "Dify", "version": "1.3.0"}, 34 | }, 35 | } 36 | 37 | elif data.get("method") == "notifications/initialized": 38 | return Response("", status=202, content_type="application/json") 39 | 40 | elif data.get("method") == "tools/list": 41 | response = { 42 | "jsonrpc": "2.0", 43 | "id": data.get("id"), 44 | "result": {"tools": [tool]}, 45 | } 46 | 47 | elif data.get("method") == "tools/call": 48 | tool_name = data.get("params", {}).get("name") 49 | arguments = data.get("params", {}).get("arguments", {}) 50 | 51 | try: 52 | if tool_name == tool.get("name"): 53 | if settings.get("app-type") == "chat": 54 | result = self.session.app.chat.invoke( 55 | app_id=app_id, 56 | query=arguments.get("query", "empty query"), 57 | inputs=arguments, 58 | response_mode="blocking", 59 | ) 60 | else: 61 | result = self.session.app.workflow.invoke( 62 | app_id=app_id, inputs=arguments, response_mode="blocking" 63 | ) 64 | else: 65 | raise ValueError(f"Unknown tool: {tool_name}") 66 | 67 | if settings.get("app-type") == "chat": 68 | final_result = {"type": "text", "text": result.get("answer")} 69 | else: 70 | r = [ 71 | v 72 | for v in result.get("data").get("outputs", {}).values() 73 | if isinstance(v, str) 74 | ] 75 | final_result = {"type": "text", "text": "\n".join(r)} 76 | 77 | response = { 78 | "jsonrpc": "2.0", 79 | "id": data.get("id"), 80 | "result": {"content": [final_result], "isError": False}, 81 | } 82 | except Exception as e: 83 | response = { 84 | "jsonrpc": "2.0", 85 | "id": data.get("id"), 86 | "error": {"code": -32000, "message": str(e)}, 87 | } 88 | else: 89 | response = { 90 | "jsonrpc": "2.0", 91 | "id": data.get("id"), 92 | "error": {"code": -32001, "message": "unsupported method"}, 93 | } 94 | 95 | self.session.storage.set(session_id, json.dumps(response).encode()) 96 | return Response("", status=202, content_type="application/json") 97 | -------------------------------------------------------------------------------- /endpoints/messages.yaml: -------------------------------------------------------------------------------- 1 | path: "/messages/" 2 | method: "POST" 3 | extra: 4 | python: 5 | source: "endpoints/messages.py" 6 | -------------------------------------------------------------------------------- /endpoints/sse.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import time 3 | import json 4 | from typing import Mapping 5 | from werkzeug import Request, Response 6 | from dify_plugin import Endpoint 7 | 8 | 9 | def create_sse_message(event, data): 10 | return f"event: {event}\ndata: {json.dumps(data) if isinstance(data, (dict, list)) else data}\n\n" 11 | 12 | 13 | class SSEEndpoint(Endpoint): 14 | def _invoke(self, r: Request, values: Mapping, settings: Mapping) -> Response: 15 | """ 16 | Invokes the endpoint with the given request. 17 | """ 18 | session_id = str(uuid.uuid4()).replace("-", "") 19 | 20 | def generate(): 21 | endpoint = f"messages/?session_id={session_id}" 22 | yield create_sse_message("endpoint", endpoint) 23 | 24 | while True: 25 | message = None 26 | if self.session.storage.exist(session_id): 27 | message = self.session.storage.get(session_id) 28 | message = message.decode() 29 | self.session.storage.delete(session_id) 30 | yield create_sse_message("message", message) 31 | time.sleep(0.5) 32 | 33 | return Response(generate(), status=200, content_type="text/event-stream") 34 | -------------------------------------------------------------------------------- /endpoints/sse.yaml: -------------------------------------------------------------------------------- 1 | path: "/sse" 2 | method: "GET" 3 | extra: 4 | python: 5 | source: "endpoints/sse.py" 6 | -------------------------------------------------------------------------------- /group/mcp-server.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | - name: app 3 | type: app-selector 4 | scope: all 5 | required: true 6 | label: 7 | en_US: App 8 | placeholder: 9 | en_US: Please select an app 10 | zh_Hans: 请选择一个应用 11 | - name: app-type 12 | type: select 13 | required: true 14 | label: 15 | en_US: App Type 16 | options: 17 | - label: 18 | en_US: Chat 19 | value: chat 20 | - label: 21 | en_US: Workflow 22 | value: workflow 23 | - name: app-input-schema 24 | type: text-input 25 | required: true 26 | label: 27 | en_US: App Input Schema 28 | placeholder: 29 | en_US: the json format of your app input schema 30 | zh_Hans: 请输入应用所需输入的 json 格式 31 | endpoints: 32 | - endpoints/sse.yaml 33 | - endpoints/messages.yaml 34 | - endpoints/http_get.yaml 35 | - endpoints/http_post.yaml 36 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from dify_plugin import Plugin, DifyPluginEnv 2 | 3 | plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120)) 4 | 5 | if __name__ == "__main__": 6 | plugin.run() 7 | -------------------------------------------------------------------------------- /manifest.yaml: -------------------------------------------------------------------------------- 1 | version: 0.0.3 2 | type: plugin 3 | author: hjlarry 4 | name: mcp-server 5 | label: 6 | en_US: mcp-server 7 | ja_JP: mcp-server 8 | zh_Hans: mcp-server 9 | pt_BR: mcp-server 10 | description: 11 | en_US: make dify's workflow as a MCP server 12 | ja_JP: make dify's workflow as a MCP server 13 | zh_Hans: make dify's workflow as a MCP server 14 | pt_BR: make dify's workflow as a MCP server 15 | icon: icon.svg 16 | resource: 17 | memory: 268435456 18 | permission: 19 | endpoint: 20 | enabled: true 21 | app: 22 | enabled: true 23 | storage: 24 | enabled: true 25 | size: 1048576 26 | plugins: 27 | endpoints: 28 | - group/mcp-server.yaml 29 | meta: 30 | version: 0.0.1 31 | arch: 32 | - amd64 33 | - arm64 34 | runner: 35 | language: python 36 | version: "3.12" 37 | entrypoint: main 38 | minimum_dify_version: 1.3.0 39 | created_at: 2025-03-17T16:01:01.118569+08:00 40 | privacy: PRIVACY.md 41 | verified: false 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dify_plugin>=0.2.1,<0.3.0 2 | --------------------------------------------------------------------------------