# Directory Structure
```
├── .gitignore
├── .python-version
├── Dockerfile
├── pyproject.toml
├── README.md
├── smithery.yaml
├── src
│ └── dify_mcp_server
│ ├── __init__.py
│ └── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.12
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
12 | # custom
13 | .vscode
14 | config.yaml
15 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Model Context Protocol (MCP) Server for dify workflows
2 | A simple implementation of an MCP server for using [dify](https://github.com/langgenius/dify). It achieves the invocation of the Dify workflow by calling the tools of MCP.
3 | ## 📰 News
4 | * [2025/4/15] zNow supports directly using environment variables to pass `base_url` and `app_sks`, making it more convenient to use with cloud-hosted platforms.
5 |
6 |
7 | ## 🔨Installation
8 | The server can be installed via [Smithery](https://smithery.ai/server/dify-mcp-server) or manually.
9 |
10 | ### Step1: prepare config.yaml or enviroments
11 | You can configure the server using either environment variables or a `config.yaml` file.
12 |
13 | #### Method 1: Using Environment Variables (Recommended for Cloud Platforms)
14 |
15 | Set the following environment variables:
16 |
17 | ```shell
18 | export DIFY_BASE_URL="https://cloud.dify.ai/v1"
19 | export DIFY_APP_SKS="app-sk1,app-sk2" # Comma-separated list of your Dify App SKs
20 | ```
21 |
22 | * `DIFY_BASE_URL`: The base URL for your Dify API.
23 | * `DIFY_APP_SKS`: A comma-separated list of your Dify App Secret Keys (SKs). Each SK typically corresponds to a different Dify workflow you want to make available via MCP.
24 |
25 | #### Method 2: Using `config.yaml`
26 |
27 | Create a `config.yaml` file to store your Dify base URL and App SKs.
28 |
29 | Example `config.yaml`:
30 |
31 | ```yaml
32 | dify_base_url: "https://cloud.dify.ai/v1"
33 | dify_app_sks:
34 | - "app-sk1" # SK for workflow 1
35 | - "app-sk2" # SK for workflow 2
36 | # Add more SKs as needed
37 | ```
38 |
39 | * `dify_base_url`: The base URL for your Dify API.
40 | * `dify_app_sks`: A list of your Dify App Secret Keys (SKs). Each SK typically corresponds to a different Dify workflow.
41 |
42 | You can create this file quickly using the following command (adjust the path and values as needed):
43 |
44 | ```bash
45 | # Create a directory if it doesn't exist
46 | mkdir -p ~/.config/dify-mcp-server
47 |
48 | # Create the config file
49 | cat > ~/.config/dify-mcp-server/config.yaml <<EOF
50 | dify_base_url: "https://cloud.dify.ai/v1"
51 | dify_app_sks:
52 | - "app-your-sk-1"
53 | - "app-your-sk-2"
54 | EOF
55 |
56 | echo "Configuration file created at ~/.config/dify-mcp-server/config.yaml"
57 | ```
58 |
59 | When running the server (as shown in Step 2), you will need to provide the path to this `config.yaml` file via the `CONFIG_PATH` environment variable if you choose this method.
60 |
61 | ### Step2: Installation on your client
62 | ❓ If you haven't installed uv or uvx yet, you can do it quickly with the following command:
63 | ```
64 | curl -Ls https://astral.sh/uv/install.sh | sh
65 | ```
66 |
67 | #### ✅ Method 1: Use uvx (no need to clone code, recommended)
68 |
69 | ```json
70 | {
71 | "mcpServers": {
72 | "dify-mcp-server": {
73 | "command": "uvx",
74 | "args": [
75 | "--from","git+https://github.com/YanxingLiu/dify-mcp-server","dify_mcp_server"
76 | ],
77 | "env": {
78 | "DIFY_BASE_URL": "https://cloud.dify.ai/v1",
79 | "DIFY_APP_SKS": "app-sk1,app-sk2",
80 | }
81 | }
82 | }
83 | }
84 | ```
85 | or
86 | ```json
87 | {
88 | "mcpServers": {
89 | "dify-mcp-server": {
90 | "command": "uvx",
91 | "args": [
92 | "--from","git+https://github.com/YanxingLiu/dify-mcp-server","dify_mcp_server"
93 | ],
94 | "env": {
95 | "CONFIG_PATH": "/Users/lyx/Downloads/config.yaml"
96 | }
97 | }
98 | }
99 | }
100 | ```
101 |
102 | #### ✅ Method 2: Use uv (local clone + uv start)
103 |
104 | You can also run the dify mcp server manually in your clients. The config of client should like the following format:
105 | ```json
106 | {
107 | "mcpServers": {
108 | "mcp-server-rag-web-browser": {
109 | "command": "uv",
110 | "args": [
111 | "--directory", "${DIFY_MCP_SERVER_PATH}",
112 | "run", "dify_mcp_server"
113 | ],
114 | "env": {
115 | "CONFIG_PATH": "$CONFIG_PATH"
116 | }
117 | }
118 | }
119 | }
120 | ```
121 | or
122 | ```json
123 | {
124 | "mcpServers": {
125 | "mcp-server-rag-web-browser": {
126 | "command": "uv",
127 | "args": [
128 | "--directory", "${DIFY_MCP_SERVER_PATH}",
129 | "run", "dify_mcp_server"
130 | ],
131 | "env": {
132 | "CONFIG_PATH": "$CONFIG_PATH"
133 | }
134 | }
135 | }
136 | }
137 | ```
138 | Example config:
139 | ```json
140 | {
141 | "mcpServers": {
142 | "dify-mcp-server": {
143 | "command": "uv",
144 | "args": [
145 | "--directory", "/Users/lyx/Downloads/dify-mcp-server",
146 | "run", "dify_mcp_server"
147 | ],
148 | "env": {
149 | "DIFY_BASE_URL": "https://cloud.dify.ai/v1",
150 | "DIFY_APP_SKS": "app-sk1,app-sk2",
151 | }
152 | }
153 | }
154 | }
155 | ```
156 | ### Enjoy it
157 | At last, you can use dify tools in any client who supports mcp.
158 |
```
--------------------------------------------------------------------------------
/src/dify_mcp_server/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from . import server
2 | import asyncio
3 |
4 | def main():
5 | """Main entry point for the package."""
6 | asyncio.run(server.main())
7 |
8 | # Optionally expose other important items at package level
9 | __all__ = ['main', 'server']
10 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "dify-mcp-server"
3 | version = "0.1.1"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | dependencies = [
8 | "httpx>=0.28.1",
9 | "mcp>=1.1.2",
10 | "omegaconf>=2.3.0",
11 | "pip>=24.3.1",
12 | "python-dotenv>=1.0.1",
13 | "requests",
14 | ]
15 |
16 | [build-system]
17 | requires = [ "hatchling",]
18 | build-backend = "hatchling.build"
19 |
20 | [project.scripts]
21 | dify_mcp_server = "dify_mcp_server:main"
22 |
23 |
24 | [tool.hatch.build.targets.wheel]
25 | packages = ["src/dify_mcp_server"]
26 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - configPath
10 | properties:
11 | configPath:
12 | type: string
13 | description: The file path to the configuration YAML file.
14 | commandFunction:
15 | # A function that produces the CLI command to start the MCP on stdio.
16 | |-
17 | (config) => ({ command: 'uv', args: ['--directory', '.', 'run', 'dify_mcp_server'], env: { CONFIG_PATH: config.configPath } })
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Use a Python image
3 | FROM python:3.12-slim
4 |
5 | # Set the working directory
6 | WORKDIR /app
7 |
8 | # Copy pyproject.toml and uv.lock to the working directory
9 | COPY pyproject.toml uv.lock /app/
10 |
11 | # Install the project's dependencies using a package manager that understands pyproject.toml
12 | RUN pip install --no-cache-dir hatchling && hatch build && pip install --no-cache-dir dist/*.whl
13 |
14 | # Copy the source files to the container
15 | COPY src/dify_mcp_server /app/src/dify_mcp_server
16 |
17 | # Set environment variables, you should provide CONFIG_PATH during container run
18 | ENV DIFY_BASE_URL="https://cloud.dify.ai/v1"
19 | ENV DIFY_APP_SKS="app-sk1,app-sk2"
20 | # ENV CONFIG_PATH=/path/to/config.yaml
21 |
22 | # Set the entrypoint
23 | ENTRYPOINT ["dify_mcp_server"]
24 |
25 | # The command to run the server
26 | CMD ["run"]
```
--------------------------------------------------------------------------------
/src/dify_mcp_server/server.py:
--------------------------------------------------------------------------------
```python
1 | import asyncio
2 | import json
3 | import os
4 | from abc import ABC
5 |
6 | import mcp.server.stdio
7 | import mcp.types as types
8 | import requests
9 | from mcp.server import NotificationOptions, Server
10 | from mcp.server.models import InitializationOptions
11 | from omegaconf import OmegaConf
12 |
13 |
14 | def get_app_info():
15 | config_path = os.getenv("CONFIG_PATH")
16 | base_url = os.getenv("DIFY_BASE_URL")
17 | dify_app_sks = os.getenv("DIFY_APP_SKS")
18 | if config_path is not None:
19 | print(f"Loading config from {config_path}")
20 | config = OmegaConf.load(config_path)
21 | dify_base_url = config.get('dify_base_url', "https://api.dify.ai/v1")
22 | dify_app_sks = config.get('dify_app_sks', [])
23 | return dify_base_url, dify_app_sks
24 | elif base_url is not None and dify_app_sks is not None:
25 | print(f"Loading config from env variables")
26 | dify_base_url = base_url
27 | dify_app_sks = dify_app_sks.split(",")
28 | return dify_base_url, dify_app_sks
29 |
30 | class DifyAPI(ABC):
31 | def __init__(self,
32 | base_url: str,
33 | dify_app_sks: list,
34 | user="default_user"):
35 | # dify configs
36 | self.dify_base_url = base_url
37 | self.dify_app_sks = dify_app_sks
38 | self.user = user
39 |
40 | # dify app infos
41 | dify_app_infos = []
42 | dify_app_params = []
43 | dify_app_metas = []
44 | for key in self.dify_app_sks:
45 | dify_app_infos.append(self.get_app_info(key))
46 | dify_app_params.append(self.get_app_parameters(key))
47 | dify_app_metas.append(self.get_app_meta(key))
48 | self.dify_app_infos = dify_app_infos
49 | self.dify_app_params = dify_app_params
50 | self.dify_app_metas = dify_app_metas
51 | self.dify_app_names = [x['name'] for x in dify_app_infos]
52 |
53 | def chat_message(
54 | self,
55 | api_key,
56 | inputs={},
57 | response_mode="streaming",
58 | conversation_id=None,
59 | user="default_user",
60 | files=None,):
61 | url = f"{self.dify_base_url}/workflows/run"
62 | headers = {
63 | "Authorization": f"Bearer {api_key}",
64 | "Content-Type": "application/json"
65 | }
66 | data = {
67 | "inputs": inputs,
68 | "response_mode": response_mode,
69 | "user": user,
70 | }
71 | if conversation_id:
72 | data["conversation_id"] = conversation_id
73 | if files:
74 | files_data = []
75 | for file_info in files:
76 | file_path = file_info.get('path')
77 | transfer_method = file_info.get('transfer_method')
78 | if transfer_method == 'local_file':
79 | files_data.append(('file', open(file_path, 'rb')))
80 | elif transfer_method == 'remote_url':
81 | pass
82 | response = requests.post(
83 | url, headers=headers, data=data, files=files_data, stream=response_mode == "streaming")
84 | else:
85 | response = requests.post(
86 | url, headers=headers, json=data, stream=response_mode == "streaming")
87 | response.raise_for_status()
88 | if response_mode == "streaming":
89 | for line in response.iter_lines():
90 | if line:
91 | if line.startswith(b'data:'):
92 | try:
93 | json_data = json.loads(line[5:].decode('utf-8'))
94 | yield json_data
95 | except json.JSONDecodeError:
96 | print(f"Error decoding JSON: {line}")
97 | else:
98 | return response.json()
99 |
100 | def upload_file(
101 | self,
102 | api_key,
103 | file_path,
104 | user="default_user"):
105 |
106 | url = f"{self.dify_base_url}/files/upload"
107 | headers = {
108 | "Authorization": f"Bearer {api_key}"
109 | }
110 | files = {
111 | "file": open(file_path, "rb")
112 | }
113 | data = {
114 | "user": user
115 | }
116 | response = requests.post(url, headers=headers, files=files, data=data)
117 | response.raise_for_status()
118 | return response.json()
119 |
120 | def stop_response(
121 | self,
122 | api_key,
123 | task_id,
124 | user="default_user"):
125 |
126 | url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
127 | headers = {
128 | "Authorization": f"Bearer {api_key}",
129 | "Content-Type": "application/json"
130 | }
131 | data = {
132 | "user": user
133 | }
134 | response = requests.post(url, headers=headers, json=data)
135 | response.raise_for_status()
136 | return response.json()
137 |
138 | def get_app_info(
139 | self,
140 | api_key,
141 | user="default_user"):
142 |
143 | url = f"{self.dify_base_url}/info"
144 | headers = {
145 | "Authorization": f"Bearer {api_key}"
146 | }
147 | params = {
148 | "user": user
149 | }
150 | response = requests.get(url, headers=headers, params=params)
151 | response.raise_for_status()
152 | return response.json()
153 |
154 | def get_app_parameters(
155 | self,
156 | api_key,
157 | user="default_user"):
158 | url = f"{self.dify_base_url}/parameters"
159 | headers = {
160 | "Authorization": f"Bearer {api_key}"
161 | }
162 | params = {
163 | "user": user
164 | }
165 | response = requests.get(url, headers=headers, params=params)
166 | response.raise_for_status()
167 | return response.json()
168 |
169 | def get_app_meta(
170 | self,
171 | api_key,
172 | user="default_user"):
173 | url = f"{self.dify_base_url}/meta"
174 | headers = {
175 | "Authorization": f"Bearer {api_key}"
176 | }
177 | params = {
178 | "user": user
179 | }
180 | response = requests.get(url, headers=headers, params=params)
181 | response.raise_for_status()
182 | return response.json()
183 |
184 |
185 | base_url, dify_app_sks = get_app_info()
186 | server = Server("dify_mcp_server")
187 | dify_api = DifyAPI(base_url, dify_app_sks)
188 |
189 |
190 | @server.list_tools()
191 | async def handle_list_tools() -> list[types.Tool]:
192 | """
193 | List available tools.
194 | Each tool specifies its arguments using JSON Schema validation.
195 | """
196 | tools = []
197 | tool_names = dify_api.dify_app_names
198 | tool_infos = dify_api.dify_app_infos
199 | tool_params = dify_api.dify_app_params
200 | tool_num = len(tool_names)
201 | for i in range(tool_num):
202 | # 0. load app info for each tool
203 | app_info = tool_infos[i]
204 | # 1. load app param for each tool
205 | inputSchema = dict(
206 | type="object",
207 | properties={},
208 | required=[],
209 | )
210 | app_param = tool_params[i]
211 | property_num = len(app_param['user_input_form'])
212 | if property_num > 0:
213 | for j in range(property_num):
214 | param = app_param['user_input_form'][j]
215 | # TODO: Add readme about strange dify user input param format
216 | param_type = list(param.keys())[0]
217 | param_info = param[param_type]
218 | property_name = param_info['variable']
219 | inputSchema["properties"][property_name] = dict(
220 | type=param_type,
221 | description=param_info['label'],
222 | )
223 | if param_info['required']:
224 | inputSchema['required'].append(property_name)
225 |
226 | tools.append(
227 | types.Tool(
228 | name=app_info['name'],
229 | description=app_info['description'],
230 | inputSchema=inputSchema,
231 | )
232 | )
233 | return tools
234 |
235 |
236 | @server.call_tool()
237 | async def handle_call_tool(
238 | name: str, arguments: dict | None
239 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
240 | tool_names = dify_api.dify_app_names
241 | if name in tool_names:
242 | tool_idx = tool_names.index(name)
243 | tool_sk = dify_api.dify_app_sks[tool_idx]
244 | responses = dify_api.chat_message(
245 | tool_sk,
246 | arguments,
247 | )
248 | for res in responses:
249 | if res['event'] == 'workflow_finished':
250 | outputs = res['data']['outputs']
251 | mcp_out = []
252 | for _, v in outputs.items():
253 | mcp_out.append(
254 | types.TextContent(
255 | type='text',
256 | text=v
257 | )
258 | )
259 | return mcp_out
260 | else:
261 | raise ValueError(f"Unknown tool: {name}")
262 |
263 |
264 | async def main():
265 | # Run the server using stdin/stdout streams
266 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
267 | await server.run(
268 | read_stream,
269 | write_stream,
270 | InitializationOptions(
271 | server_name="dify_mcp_server",
272 | server_version="0.1.0",
273 | capabilities=server.get_capabilities(
274 | notification_options=NotificationOptions(),
275 | experimental_capabilities={},
276 | ),
277 | ),
278 | )
279 |
280 | if __name__ == "__main__":
281 | asyncio.run(main())
282 |
```