#
tokens: 4032/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | ## 🔨Installation
 4 | The server can be installed via [Smithery](https://smithery.ai/server/dify-mcp-server) or manually. Config.yaml is required for both methods. Thus, we need to prepare it before installation.
 5 | 
 6 | ### Prepare config.yaml
 7 | Before using the mcp server, you should prepare a config.yaml to save your dify_base_url and dify_sks. The example config like this:
 8 | ```yaml
 9 | dify_base_url: "https://cloud.dify.ai/v1"
10 | dify_app_sks:
11 |   - "app-sk1"
12 |   - "app-sk2"
13 | ```
14 | Different SKs correspond to different dify workflows.
15 | ### Installing via Smithery
16 | [smithery](https://smithery.ai) is a tool to install the dify mcp server automatically.
17 | To install Dify MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/dify-mcp-server):
18 | 
19 | ```bash
20 | npx -y @smithery/cli install dify-mcp-server --client claude
21 | ```
22 | 
23 | ### Manual Installation
24 | You can also run the dify mcp server manually in your clients. The config of client should like the following format:
25 | ```json
26 | "mcpServers": {
27 |   "mcp-server-rag-web-browser": {
28 |     "command": "uv",
29 |       "args": [
30 |         "--directory", "${DIFY_MCP_SERVER_PATH}",
31 |         "run", "dify_mcp_server"
32 |       ],
33 |     "env": {
34 |        "CONFIG_PATH": "$CONFIG_PATH"
35 |     }
36 |   }
37 | }
38 | ```
39 | Example config:
40 | ```json
41 | "mcpServers": {
42 |   "mcp-server-rag-web-browser": {
43 |     "command": "uv",
44 |       "args": [
45 |         "--directory", "/Users/lyx/Downloads/dify-mcp-server",
46 |         "run", "dify_mcp_server"
47 |       ],
48 |     "env": {
49 |        "CONFIG_PATH": "/Users/lyx/Downloads/config.yaml"
50 |     }
51 |   }
52 | }
53 | ```
54 | ### Enjoy it
55 | At last, you can use dify tools in any client who supports mcp.
56 | 
```

--------------------------------------------------------------------------------
/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.0"
 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 | 
```

--------------------------------------------------------------------------------
/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 CONFIG_PATH=/path/to/config.yaml
19 | 
20 | # Set the entrypoint
21 | ENTRYPOINT ["dify_mcp_server"]
22 | 
23 | # The command to run the server
24 | 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 | class DifyAPI(ABC):
 15 |     def __init__(self,
 16 |                  config_path,
 17 |                  user="default_user"):
 18 |         if not config_path:
 19 |             raise ValueError("config path not provided")
 20 |         self.config = OmegaConf.load(config_path)
 21 | 
 22 |         # dify configs
 23 |         self.dify_base_url = self.config.dify_base_url
 24 |         self.dify_app_sks = self.config.dify_app_sks
 25 |         self.user = user
 26 | 
 27 |         # dify app infos
 28 |         dify_app_infos = []
 29 |         dify_app_params = []
 30 |         dify_app_metas = []
 31 |         for key in self.dify_app_sks:
 32 |             dify_app_infos.append(self.get_app_info(key))
 33 |             dify_app_params.append(self.get_app_parameters(key))
 34 |             dify_app_metas.append(self.get_app_meta(key))
 35 |         self.dify_app_infos = dify_app_infos
 36 |         self.dify_app_params = dify_app_params
 37 |         self.dify_app_metas = dify_app_metas
 38 |         self.dify_app_names = [x['name'] for x in dify_app_infos]
 39 | 
 40 |     def chat_message(
 41 |             self,
 42 |             api_key,
 43 |             inputs={},
 44 |             response_mode="streaming",
 45 |             conversation_id=None,
 46 |             user="default_user",
 47 |             files=None,):
 48 |         url = f"{self.dify_base_url}/workflows/run"
 49 |         headers = {
 50 |             "Authorization": f"Bearer {api_key}",
 51 |             "Content-Type": "application/json"
 52 |         }
 53 |         data = {
 54 |             "inputs": inputs,
 55 |             "response_mode": response_mode,
 56 |             "user": user,
 57 |         }
 58 |         if conversation_id:
 59 |             data["conversation_id"] = conversation_id
 60 |         if files:
 61 |             files_data = []
 62 |             for file_info in files:
 63 |                 file_path = file_info.get('path')
 64 |                 transfer_method = file_info.get('transfer_method')
 65 |                 if transfer_method == 'local_file':
 66 |                     files_data.append(('file', open(file_path, 'rb')))
 67 |                 elif transfer_method == 'remote_url':
 68 |                     pass
 69 |             response = requests.post(
 70 |                 url, headers=headers, data=data, files=files_data, stream=response_mode == "streaming")
 71 |         else:
 72 |             response = requests.post(
 73 |                 url, headers=headers, json=data, stream=response_mode == "streaming")
 74 |         response.raise_for_status()
 75 |         if response_mode == "streaming":
 76 |             for line in response.iter_lines():
 77 |                 if line:
 78 |                     if line.startswith(b'data:'):
 79 |                         try:
 80 |                             json_data = json.loads(line[5:].decode('utf-8'))
 81 |                             yield json_data
 82 |                         except json.JSONDecodeError:
 83 |                             print(f"Error decoding JSON: {line}")
 84 |         else:
 85 |             return response.json()
 86 | 
 87 |     def upload_file(
 88 |             self,
 89 |             api_key,
 90 |             file_path,
 91 |             user="default_user"):
 92 | 
 93 |         url = f"{self.dify_base_url}/files/upload"
 94 |         headers = {
 95 |             "Authorization": f"Bearer {api_key}"
 96 |         }
 97 |         files = {
 98 |             "file": open(file_path, "rb")
 99 |         }
100 |         data = {
101 |             "user": user
102 |         }
103 |         response = requests.post(url, headers=headers, files=files, data=data)
104 |         response.raise_for_status()
105 |         return response.json()
106 | 
107 |     def stop_response(
108 |             self,
109 |             api_key,
110 |             task_id,
111 |             user="default_user"):
112 | 
113 |         url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
114 |         headers = {
115 |             "Authorization": f"Bearer {api_key}",
116 |             "Content-Type": "application/json"
117 |         }
118 |         data = {
119 |             "user": user
120 |         }
121 |         response = requests.post(url, headers=headers, json=data)
122 |         response.raise_for_status()
123 |         return response.json()
124 | 
125 |     def get_app_info(
126 |             self,
127 |             api_key,
128 |             user="default_user"):
129 | 
130 |         url = f"{self.dify_base_url}/info"
131 |         headers = {
132 |             "Authorization": f"Bearer {api_key}"
133 |         }
134 |         params = {
135 |             "user": user
136 |         }
137 |         response = requests.get(url, headers=headers, params=params)
138 |         response.raise_for_status()
139 |         return response.json()
140 | 
141 |     def get_app_parameters(
142 |             self,
143 |             api_key,
144 |             user="default_user"):
145 |         url = f"{self.dify_base_url}/parameters"
146 |         headers = {
147 |             "Authorization": f"Bearer {api_key}"
148 |         }
149 |         params = {
150 |             "user": user
151 |         }
152 |         response = requests.get(url, headers=headers, params=params)
153 |         response.raise_for_status()
154 |         return response.json()
155 | 
156 |     def get_app_meta(
157 |             self,
158 |             api_key,
159 |             user="default_user"):
160 |         url = f"{self.dify_base_url}/meta"
161 |         headers = {
162 |             "Authorization": f"Bearer {api_key}"
163 |         }
164 |         params = {
165 |             "user": user
166 |         }
167 |         response = requests.get(url, headers=headers, params=params)
168 |         response.raise_for_status()
169 |         return response.json()
170 | 
171 | 
172 | config_path = os.getenv("CONFIG_PATH")
173 | server = Server("dify_mcp_server")
174 | dify_api = DifyAPI(config_path)
175 | 
176 | 
177 | @server.list_tools()
178 | async def handle_list_tools() -> list[types.Tool]:
179 |     """
180 |     List available tools.
181 |     Each tool specifies its arguments using JSON Schema validation.
182 |     """
183 |     tools = []
184 |     tool_names = dify_api.dify_app_names
185 |     tool_infos = dify_api.dify_app_infos
186 |     tool_params = dify_api.dify_app_params
187 |     tool_num = len(tool_names)
188 |     for i in range(tool_num):
189 |         # 0. load app info for each tool
190 |         app_info = tool_infos[i]
191 |         # 1. load app param for each tool
192 |         inputSchema = dict(
193 |             type="object",
194 |             properties={},
195 |             required=[],
196 |         )
197 |         app_param = tool_params[i]
198 |         property_num = len(app_param['user_input_form'])
199 |         if property_num > 0:
200 |             for j in range(property_num):
201 |                 param = app_param['user_input_form'][j]
202 |                 # TODO: Add readme about strange dify user input param format
203 |                 param_type = list(param.keys())[0]
204 |                 param_info = param[param_type]
205 |                 property_name = param_info['variable']
206 |                 inputSchema["properties"][property_name] = dict(
207 |                     type=param_type,
208 |                     description=param_info['label'],
209 |                 )
210 |                 if param_info['required']:
211 |                     inputSchema['required'].append(property_name)
212 | 
213 |         tools.append(
214 |             types.Tool(
215 |                 name=app_info['name'],
216 |                 description=app_info['description'],
217 |                 inputSchema=inputSchema,
218 |             )
219 |         )
220 |     return tools
221 | 
222 | 
223 | @server.call_tool()
224 | async def handle_call_tool(
225 |     name: str, arguments: dict | None
226 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
227 |     tool_names = dify_api.dify_app_names
228 |     if name in tool_names:
229 |         tool_idx = tool_names.index(name)
230 |         tool_sk = dify_api.dify_app_sks[tool_idx]
231 |         responses = dify_api.chat_message(
232 |             tool_sk,
233 |             arguments,
234 |         )
235 |         for res in responses:
236 |             if res['event'] == 'workflow_finished':
237 |                 outputs = res['data']['outputs']
238 |         mcp_out = []
239 |         for _, v in outputs.items():
240 |             mcp_out.append(
241 |                 types.TextContent(
242 |                     type='text',
243 |                     text=v
244 |                 )
245 |             )
246 |         return mcp_out
247 |     else:
248 |         raise ValueError(f"Unknown tool: {name}")
249 | 
250 | 
251 | async def main():
252 |     # Run the server using stdin/stdout streams
253 |     async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
254 |         await server.run(
255 |             read_stream,
256 |             write_stream,
257 |             InitializationOptions(
258 |                 server_name="dify_mcp_server",
259 |                 server_version="0.1.0",
260 |                 capabilities=server.get_capabilities(
261 |                     notification_options=NotificationOptions(),
262 |                     experimental_capabilities={},
263 |                 ),
264 |             ),
265 |         )
266 | 
267 | if __name__ == "__main__":
268 |     asyncio.run(main())
269 | 
```