#
tokens: 4316/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```