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