mseep-dify-mcp-server 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: mseep-dify-mcp-server
|
3
|
+
Version: 0.1.1
|
4
|
+
Summary: Add your description here
|
5
|
+
Requires-Python: >=3.10
|
6
|
+
Requires-Dist: httpx>=0.28.1
|
7
|
+
Requires-Dist: mcp>=1.1.2
|
8
|
+
Requires-Dist: omegaconf>=2.3.0
|
9
|
+
Requires-Dist: pip>=24.3.1
|
10
|
+
Requires-Dist: python-dotenv>=1.0.1
|
11
|
+
Requires-Dist: requests
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
|
14
|
+
# Model Context Protocol (MCP) Server for dify workflows
|
15
|
+
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.
|
16
|
+
## 📰 News
|
17
|
+
* [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.
|
18
|
+
|
19
|
+
|
20
|
+
## 🔨Installation
|
21
|
+
The server can be installed via [Smithery](https://smithery.ai/server/dify-mcp-server) or manually.
|
22
|
+
|
23
|
+
### Step1: prepare config.yaml or enviroments
|
24
|
+
You can configure the server using either environment variables or a `config.yaml` file.
|
25
|
+
|
26
|
+
#### Method 1: Using Environment Variables (Recommended for Cloud Platforms)
|
27
|
+
|
28
|
+
Set the following environment variables:
|
29
|
+
|
30
|
+
```shell
|
31
|
+
export DIFY_BASE_URL="https://cloud.dify.ai/v1"
|
32
|
+
export DIFY_APP_SKS="app-sk1,app-sk2" # Comma-separated list of your Dify App SKs
|
33
|
+
```
|
34
|
+
|
35
|
+
* `DIFY_BASE_URL`: The base URL for your Dify API.
|
36
|
+
* `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.
|
37
|
+
|
38
|
+
#### Method 2: Using `config.yaml`
|
39
|
+
|
40
|
+
Create a `config.yaml` file to store your Dify base URL and App SKs.
|
41
|
+
|
42
|
+
Example `config.yaml`:
|
43
|
+
|
44
|
+
```yaml
|
45
|
+
dify_base_url: "https://cloud.dify.ai/v1"
|
46
|
+
dify_app_sks:
|
47
|
+
- "app-sk1" # SK for workflow 1
|
48
|
+
- "app-sk2" # SK for workflow 2
|
49
|
+
# Add more SKs as needed
|
50
|
+
```
|
51
|
+
|
52
|
+
* `dify_base_url`: The base URL for your Dify API.
|
53
|
+
* `dify_app_sks`: A list of your Dify App Secret Keys (SKs). Each SK typically corresponds to a different Dify workflow.
|
54
|
+
|
55
|
+
You can create this file quickly using the following command (adjust the path and values as needed):
|
56
|
+
|
57
|
+
```bash
|
58
|
+
# Create a directory if it doesn't exist
|
59
|
+
mkdir -p ~/.config/dify-mcp-server
|
60
|
+
|
61
|
+
# Create the config file
|
62
|
+
cat > ~/.config/dify-mcp-server/config.yaml <<EOF
|
63
|
+
dify_base_url: "https://cloud.dify.ai/v1"
|
64
|
+
dify_app_sks:
|
65
|
+
- "app-your-sk-1"
|
66
|
+
- "app-your-sk-2"
|
67
|
+
EOF
|
68
|
+
|
69
|
+
echo "Configuration file created at ~/.config/dify-mcp-server/config.yaml"
|
70
|
+
```
|
71
|
+
|
72
|
+
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.
|
73
|
+
|
74
|
+
### Step2: Installation on your client
|
75
|
+
❓ If you haven't installed uv or uvx yet, you can do it quickly with the following command:
|
76
|
+
```
|
77
|
+
curl -Ls https://astral.sh/uv/install.sh | sh
|
78
|
+
```
|
79
|
+
|
80
|
+
#### ✅ Method 1: Use uvx (no need to clone code, recommended)
|
81
|
+
|
82
|
+
```json
|
83
|
+
{
|
84
|
+
"mcpServers": {
|
85
|
+
"dify-mcp-server": {
|
86
|
+
"command": "uvx",
|
87
|
+
"args": [
|
88
|
+
"--from","git+https://github.com/YanxingLiu/dify-mcp-server","dify_mcp_server"
|
89
|
+
],
|
90
|
+
"env": {
|
91
|
+
"DIFY_BASE_URL": "https://cloud.dify.ai/v1",
|
92
|
+
"DIFY_APP_SKS": "app-sk1,app-sk2",
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
```
|
98
|
+
or
|
99
|
+
```json
|
100
|
+
{
|
101
|
+
"mcpServers": {
|
102
|
+
"dify-mcp-server": {
|
103
|
+
"command": "uvx",
|
104
|
+
"args": [
|
105
|
+
"--from","git+https://github.com/YanxingLiu/dify-mcp-server","dify_mcp_server"
|
106
|
+
],
|
107
|
+
"env": {
|
108
|
+
"CONFIG_PATH": "/Users/lyx/Downloads/config.yaml"
|
109
|
+
}
|
110
|
+
}
|
111
|
+
}
|
112
|
+
}
|
113
|
+
```
|
114
|
+
|
115
|
+
#### ✅ Method 2: Use uv (local clone + uv start)
|
116
|
+
|
117
|
+
You can also run the dify mcp server manually in your clients. The config of client should like the following format:
|
118
|
+
```json
|
119
|
+
{
|
120
|
+
"mcpServers": {
|
121
|
+
"mcp-server-rag-web-browser": {
|
122
|
+
"command": "uv",
|
123
|
+
"args": [
|
124
|
+
"--directory", "${DIFY_MCP_SERVER_PATH}",
|
125
|
+
"run", "dify_mcp_server"
|
126
|
+
],
|
127
|
+
"env": {
|
128
|
+
"CONFIG_PATH": "$CONFIG_PATH"
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
```
|
134
|
+
or
|
135
|
+
```json
|
136
|
+
{
|
137
|
+
"mcpServers": {
|
138
|
+
"mcp-server-rag-web-browser": {
|
139
|
+
"command": "uv",
|
140
|
+
"args": [
|
141
|
+
"--directory", "${DIFY_MCP_SERVER_PATH}",
|
142
|
+
"run", "dify_mcp_server"
|
143
|
+
],
|
144
|
+
"env": {
|
145
|
+
"CONFIG_PATH": "$CONFIG_PATH"
|
146
|
+
}
|
147
|
+
}
|
148
|
+
}
|
149
|
+
}
|
150
|
+
```
|
151
|
+
Example config:
|
152
|
+
```json
|
153
|
+
{
|
154
|
+
"mcpServers": {
|
155
|
+
"dify-mcp-server": {
|
156
|
+
"command": "uv",
|
157
|
+
"args": [
|
158
|
+
"--directory", "/Users/lyx/Downloads/dify-mcp-server",
|
159
|
+
"run", "dify_mcp_server"
|
160
|
+
],
|
161
|
+
"env": {
|
162
|
+
"DIFY_BASE_URL": "https://cloud.dify.ai/v1",
|
163
|
+
"DIFY_APP_SKS": "app-sk1,app-sk2",
|
164
|
+
}
|
165
|
+
}
|
166
|
+
}
|
167
|
+
}
|
168
|
+
```
|
169
|
+
### Enjoy it
|
170
|
+
At last, you can use dify tools in any client who supports mcp.
|
@@ -0,0 +1,6 @@
|
|
1
|
+
src/dify_mcp_server/__init__.py,sha256=kTuRDcmoAa4al59WMbwXx_TjJhMqmpzCMFCZ981BL1M,213
|
2
|
+
src/dify_mcp_server/server.py,sha256=pTdMPTmvK1uO6Ki062bVSp-VQ9cBbFO2WtMSfowGgzU,8966
|
3
|
+
mseep_dify_mcp_server-0.1.1.dist-info/METADATA,sha256=Oa23r-DEa2TddtWfAR45XxjzlXKJ7qMv6LTfPzo5EBQ,4519
|
4
|
+
mseep_dify_mcp_server-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
mseep_dify_mcp_server-0.1.1.dist-info/entry_points.txt,sha256=YughlZESvmM9v4WMtY4GAUzFWY-udx_CBKQUa3WXxeU,57
|
6
|
+
mseep_dify_mcp_server-0.1.1.dist-info/RECORD,,
|
@@ -0,0 +1,281 @@
|
|
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())
|