# Directory Structure
```
├── .gitignore
├── build
│ ├── config.js
│ ├── dify-client.js
│ ├── index.js
│ └── types.js
├── Dockerfile
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ ├── config.ts
│ ├── dify-client.ts
│ ├── index.ts
│ └── types.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | config.yaml
2 |
3 | node_modules
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
2 | # Stage 1: build
3 | FROM node:lts-alpine AS builder
4 | WORKDIR /app
5 | COPY package.json package-lock.json tsconfig.json ./
6 | COPY src ./src
7 | RUN npm ci && npm run build
8 |
9 | # Stage 2: production
10 | FROM node:lts-alpine
11 | WORKDIR /app
12 | # Copy built files and dependencies
13 | COPY --from=builder /app/build ./build
14 | COPY --from=builder /app/package.json ./package.json
15 | COPY --from=builder /app/package-lock.json ./package-lock.json
16 | RUN npm ci --omit=dev && chmod +x build/index.js
17 |
18 | # Use shell entrypoint to generate config and start server
19 | ENTRYPOINT ["sh"]
20 | CMD []
21 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "dify-workflow-mcp",
3 | "version": "1.0.0",
4 | "description": "A TypeScript MCP server for Dify workflows",
5 | "type": "module",
6 | "bin": {
7 | "dify-mcp": "./build/index.js"
8 | },
9 | "scripts": {
10 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
11 | "start": "node build/index.js",
12 | "dev": "ts-node --esm src/index.ts",
13 | "inspector": "npx @modelcontextprotocol/inspector build/index.js"
14 | },
15 | "keywords": [
16 | "mcp",
17 | "dify",
18 | "typescript"
19 | ],
20 | "author": "",
21 | "license": "ISC",
22 | "dependencies": {
23 | "@modelcontextprotocol/inspector": "^0.2.7",
24 | "@modelcontextprotocol/sdk": "^1.0.0",
25 | "@types/axios": "^0.14.0",
26 | "axios": "^1.6.7",
27 | "yaml": "^2.3.4",
28 | "zod": "^3.22.4"
29 | },
30 | "devDependencies": {
31 | "@types/node": "^20.11.16",
32 | "ts-node": "^10.9.2",
33 | "typescript": "^5.3.3"
34 | }
35 | }
36 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config
2 |
3 | startCommand:
4 | type: stdio
5 | commandFunction:
6 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
7 | |-
8 | (config)=>({command:'sh',args:['-c',`cat >config.yaml <<EOF
9 | dify_base_url: "${config.difyBaseUrl}"
10 | dify_app_sks:
11 | ${config.difyAppSks.map(sk => ' - "'+sk+'"').join('\n')}
12 | EOF
13 | node build/index.js`],env:{CONFIG_PATH:'config.yaml'}})
14 | configSchema:
15 | # JSON Schema defining the configuration options for the MCP.
16 | type: object
17 | required:
18 | - difyBaseUrl
19 | - difyAppSks
20 | properties:
21 | difyBaseUrl:
22 | type: string
23 | description: Dify API base URL
24 | difyAppSks:
25 | type: array
26 | items:
27 | type: string
28 | description: List of Dify application secret keys
29 | exampleConfig:
30 | difyBaseUrl: https://api.dify.ai/v1
31 | difyAppSks:
32 | - sk-app-123
33 | - sk-app-456
34 |
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolSchema } from '@modelcontextprotocol/sdk/types.js'
2 | import { z } from 'zod'
3 |
4 | // Configuration types
5 | export interface DifyConfig {
6 | dify_base_url: string
7 | dify_app_sks: string[]
8 | }
9 |
10 | // Dify API response types
11 | export interface DifyAppInfo {
12 | name: string
13 | description: string
14 | tags: string[]
15 | }
16 |
17 | export interface DifyParameterField {
18 | label: string
19 | variable: string
20 | required: boolean
21 | default: string
22 | }
23 |
24 | export interface DifyFileUploadConfig {
25 | enabled: boolean
26 | number_limits: number
27 | detail: string
28 | transfer_methods: string[]
29 | }
30 |
31 | export interface DifySystemParameters {
32 | file_size_limit: number
33 | image_file_size_limit: number
34 | audio_file_size_limit: number
35 | video_file_size_limit: number
36 | }
37 |
38 | interface BaseInputControl {
39 | label: string
40 | variable: string
41 | required: boolean
42 | default?: string
43 | }
44 |
45 | export enum UserInputControlType {
46 | TextInput = 'text-input',
47 | ParagraphInput = 'paragraph',
48 | SelectInput = 'select',
49 | NumberInput = 'number'
50 | }
51 |
52 | export abstract class BaseUserInputForm {
53 | [key: string]: BaseInputControl
54 | }
55 |
56 | export interface TextUserInputForm extends BaseUserInputForm {
57 | [UserInputControlType.TextInput]: BaseInputControl
58 | }
59 |
60 | export interface ParagraphUserInputForm extends BaseUserInputForm {
61 | [UserInputControlType.ParagraphInput]: BaseInputControl
62 | }
63 |
64 | export interface SelectUserInputForm extends BaseUserInputForm {
65 | [UserInputControlType.SelectInput]: BaseInputControl & {
66 | options: string[]
67 | }
68 | }
69 |
70 | export interface NumberUserInputForm extends BaseUserInputForm {
71 | [UserInputControlType.NumberInput]: BaseInputControl
72 | }
73 |
74 | export type UserInputForm = TextUserInputForm | ParagraphUserInputForm | SelectUserInputForm | NumberUserInputForm
75 |
76 | export interface DifyParameters {
77 | user_input_form: UserInputForm[]
78 | file_upload: {
79 | image: DifyFileUploadConfig
80 | }
81 | system_parameters: DifySystemParameters
82 | }
83 |
84 | // MCP Tool types
85 | export type MCPToolInputSchema = Required<z.infer<typeof ToolSchema>['inputSchema']>
86 |
87 | export interface MCPTool {
88 | name: string
89 | description: string
90 | inputSchema: MCPToolInputSchema
91 | }
92 |
93 | // Dify API response types
94 | export interface DifyWorkflowResponse {
95 | answer: string
96 | task_id: string
97 | }
98 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
4 | import { z } from 'zod'
5 | import path, { dirname } from 'path';
6 | import { fileURLToPath } from 'url';
7 | import { loadConfig } from './config.js'
8 | import { DifyClient } from './dify-client.js'
9 | import {
10 | BaseUserInputForm,
11 | DifyParameters,
12 | MCPTool,
13 | MCPToolInputSchema,
14 | NumberUserInputForm,
15 | ParagraphUserInputForm,
16 | SelectUserInputForm,
17 | TextUserInputForm,
18 | UserInputControlType
19 | } from './types.js'
20 |
21 | const __filename = fileURLToPath(import.meta.url);
22 | const __dirname = dirname(__filename);
23 |
24 | // Load configuration
25 | const config = loadConfig(process.env.CONFIG_PATH || path.resolve(__dirname, '../config.yaml'))
26 |
27 | // Create server instance
28 | const server = new Server(
29 | {
30 | name: 'dify-workflow-mcp',
31 | version: '1.0.0'
32 | },
33 | {
34 | capabilities: {
35 | tools: {}
36 | }
37 | }
38 | )
39 |
40 | // cache dify parameters
41 | const difyParametersMap = new Map<string, DifyParameters>()
42 | // cache name app sks
43 | const appSkMap = new Map<string, string>()
44 | // Initialize Dify clients
45 | const difyClients = new Map<string, DifyClient>()
46 | for (const appSk of config.dify_app_sks) {
47 | const client = new DifyClient(config.dify_base_url, appSk)
48 | difyClients.set(appSk, client)
49 | }
50 |
51 | // List available tools
52 | server.setRequestHandler(ListToolsRequestSchema, async () => {
53 | const tools: MCPTool[] = []
54 |
55 | let index = 0
56 |
57 | for (const client of difyClients.values()) {
58 | try {
59 | const [appInfo, parameters] = await Promise.all([client.getAppInfo(), client.getParameters()])
60 |
61 | const inputSchema: MCPToolInputSchema = convertDifyParametersToJsonSchema(parameters)
62 |
63 | // Cache Dify parameters
64 | difyParametersMap.set(appInfo.name, parameters)
65 |
66 | // Cache app sk
67 | appSkMap.set(appInfo.name, config.dify_app_sks[index++])
68 |
69 | tools.push({
70 | name: appInfo.name,
71 | description: appInfo.description,
72 | inputSchema
73 | })
74 | } catch (error) {
75 | console.error('Failed to load tool:', error)
76 | }
77 | }
78 |
79 | return { tools }
80 | })
81 |
82 | // Handle tool execution
83 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
84 | const { name, arguments: args } = request.params
85 |
86 | try {
87 | // Find the corresponding Dify client
88 | const appSk = appSkMap.get(name)
89 | if (!appSk) {
90 | throw new Error('Unsupported tool')
91 | }
92 | const client = difyClients.get(appSk)
93 | if (!client) {
94 | throw new Error('No Dify client available')
95 | }
96 |
97 | const difyParameters = difyParametersMap.get(name)
98 | if (!difyParameters) {
99 | throw new Error('No Dify parameters available')
100 | }
101 |
102 | // Validate input parameters
103 | const validatedArgs = await validateInputParameters(args, difyParameters)
104 |
105 | // Execute the workflow
106 | const result = await client.runWorkflow(validatedArgs)
107 |
108 | return {
109 | content: [
110 | {
111 | type: 'text',
112 | text: result
113 | }
114 | ]
115 | }
116 | } catch (error) {
117 | if (error instanceof z.ZodError) {
118 | throw new Error(`Invalid arguments: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`)
119 | }
120 | throw error
121 | }
122 | })
123 |
124 | // Validate input parameters
125 | const validateInputParameters = (args: any, difyParameters: DifyParameters) => {
126 | const schema = z.object(
127 | Object.fromEntries(
128 | difyParameters.user_input_form.map((form) => {
129 | if (isParagraphInput(form)) {
130 | const { required, label, variable } = form[UserInputControlType.ParagraphInput]
131 | const currentSchema = required
132 | ? z.string({
133 | message: `${label} is required!`
134 | })
135 | : z.optional(z.string())
136 | return [variable, currentSchema]
137 | }
138 |
139 | if (isTextInput(form)) {
140 | const { required, label, variable } = form[UserInputControlType.TextInput]
141 | const currentSchema = required
142 | ? z.string({
143 | message: `${label} is required!`
144 | })
145 | : z.optional(z.string())
146 | return [variable, currentSchema]
147 | }
148 |
149 | if (isSelectInput(form)) {
150 | const { required, options, variable } = form[UserInputControlType.SelectInput]
151 | const currentSchema = required
152 | ? z.enum(options as [string, ...string[]])
153 | : z.optional(z.enum(options as [string, ...string[]]))
154 | return [variable, currentSchema]
155 | }
156 |
157 | if (isNumberInput(form)) {
158 | const { required, label, variable } = form[UserInputControlType.NumberInput]
159 | const currentSchema = required
160 | ? z.number({
161 | message: `${label} is required!`
162 | })
163 | : z.optional(z.number())
164 | return [variable, currentSchema]
165 | }
166 |
167 | throw new Error(`Invalid difyParameters`)
168 | })
169 | )
170 | )
171 | return schema.parse(args)
172 | }
173 |
174 | /**
175 | * Convert Dify parameters to JSON Schema
176 | */
177 | const convertDifyParametersToJsonSchema = (parameters: DifyParameters): MCPToolInputSchema => {
178 | const inputSchema: MCPToolInputSchema = {
179 | type: 'object',
180 | properties: {},
181 | required: []
182 | }
183 | for (const input of parameters.user_input_form) {
184 | // 处理 UserInputControlType.TextInput
185 | if (isTextInput(input)) {
186 | inputSchema.properties[input[UserInputControlType.TextInput].variable] = {
187 | type: 'string'
188 | }
189 | }
190 |
191 | // 处理 UserInputControlType.ParagraphInput
192 | if (isParagraphInput(input)) {
193 | inputSchema.properties[input[UserInputControlType.ParagraphInput].variable] = {
194 | type: 'string'
195 | }
196 | }
197 |
198 | // 处理 UserInputControlType.SelectInput
199 | if (isSelectInput(input)) {
200 | inputSchema.properties[input[UserInputControlType.SelectInput].variable] = {
201 | type: 'array',
202 | enum: input[UserInputControlType.SelectInput].options
203 | }
204 | }
205 |
206 | // 处理 UserInputControlType.NumberInput
207 | if (isNumberInput(input)) {
208 | inputSchema.properties[input[UserInputControlType.NumberInput].variable] = {
209 | type: 'number'
210 | }
211 | }
212 | }
213 | return inputSchema
214 | }
215 |
216 | const isTextInput = (input: BaseUserInputForm): input is TextUserInputForm => {
217 | return input['text'] !== undefined
218 | }
219 |
220 | const isParagraphInput = (input: BaseUserInputForm): input is ParagraphUserInputForm => {
221 | return input['paragraph'] !== undefined
222 | }
223 |
224 | const isSelectInput = (input: BaseUserInputForm): input is SelectUserInputForm => {
225 | return input['select'] !== undefined
226 | }
227 |
228 | const isNumberInput = (input: BaseUserInputForm): input is NumberUserInputForm => {
229 | return input['number'] !== undefined
230 | }
231 |
232 | // Start the server
233 | async function main() {
234 | const transport = new StdioServerTransport()
235 | await server.connect(transport)
236 | console.error('Dify MCP Server running on stdio')
237 | }
238 |
239 | main().catch((error) => {
240 | console.error('Fatal error in main():', error)
241 | process.exit(1)
242 | })
243 |
```