# 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 | 
```

