# 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:
--------------------------------------------------------------------------------
```
config.yaml
node_modules
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
# Stage 1: build
FROM node:lts-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
COPY src ./src
RUN npm ci && npm run build
# Stage 2: production
FROM node:lts-alpine
WORKDIR /app
# Copy built files and dependencies
COPY --from=builder /app/build ./build
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
RUN npm ci --omit=dev && chmod +x build/index.js
# Use shell entrypoint to generate config and start server
ENTRYPOINT ["sh"]
CMD []
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "dify-workflow-mcp",
"version": "1.0.0",
"description": "A TypeScript MCP server for Dify workflows",
"type": "module",
"bin": {
"dify-mcp": "./build/index.js"
},
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"start": "node build/index.js",
"dev": "ts-node --esm src/index.ts",
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
},
"keywords": [
"mcp",
"dify",
"typescript"
],
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/inspector": "^0.2.7",
"@modelcontextprotocol/sdk": "^1.0.0",
"@types/axios": "^0.14.0",
"axios": "^1.6.7",
"yaml": "^2.3.4",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.11.16",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/build/project-config
startCommand:
type: stdio
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config)=>({command:'sh',args:['-c',`cat >config.yaml <<EOF
dify_base_url: "${config.difyBaseUrl}"
dify_app_sks:
${config.difyAppSks.map(sk => ' - "'+sk+'"').join('\n')}
EOF
node build/index.js`],env:{CONFIG_PATH:'config.yaml'}})
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- difyBaseUrl
- difyAppSks
properties:
difyBaseUrl:
type: string
description: Dify API base URL
difyAppSks:
type: array
items:
type: string
description: List of Dify application secret keys
exampleConfig:
difyBaseUrl: https://api.dify.ai/v1
difyAppSks:
- sk-app-123
- sk-app-456
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
// Configuration types
export interface DifyConfig {
dify_base_url: string
dify_app_sks: string[]
}
// Dify API response types
export interface DifyAppInfo {
name: string
description: string
tags: string[]
}
export interface DifyParameterField {
label: string
variable: string
required: boolean
default: string
}
export interface DifyFileUploadConfig {
enabled: boolean
number_limits: number
detail: string
transfer_methods: string[]
}
export interface DifySystemParameters {
file_size_limit: number
image_file_size_limit: number
audio_file_size_limit: number
video_file_size_limit: number
}
interface BaseInputControl {
label: string
variable: string
required: boolean
default?: string
}
export enum UserInputControlType {
TextInput = 'text-input',
ParagraphInput = 'paragraph',
SelectInput = 'select',
NumberInput = 'number'
}
export abstract class BaseUserInputForm {
[key: string]: BaseInputControl
}
export interface TextUserInputForm extends BaseUserInputForm {
[UserInputControlType.TextInput]: BaseInputControl
}
export interface ParagraphUserInputForm extends BaseUserInputForm {
[UserInputControlType.ParagraphInput]: BaseInputControl
}
export interface SelectUserInputForm extends BaseUserInputForm {
[UserInputControlType.SelectInput]: BaseInputControl & {
options: string[]
}
}
export interface NumberUserInputForm extends BaseUserInputForm {
[UserInputControlType.NumberInput]: BaseInputControl
}
export type UserInputForm = TextUserInputForm | ParagraphUserInputForm | SelectUserInputForm | NumberUserInputForm
export interface DifyParameters {
user_input_form: UserInputForm[]
file_upload: {
image: DifyFileUploadConfig
}
system_parameters: DifySystemParameters
}
// MCP Tool types
export type MCPToolInputSchema = Required<z.infer<typeof ToolSchema>['inputSchema']>
export interface MCPTool {
name: string
description: string
inputSchema: MCPToolInputSchema
}
// Dify API response types
export interface DifyWorkflowResponse {
answer: string
task_id: string
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { loadConfig } from './config.js'
import { DifyClient } from './dify-client.js'
import {
BaseUserInputForm,
DifyParameters,
MCPTool,
MCPToolInputSchema,
NumberUserInputForm,
ParagraphUserInputForm,
SelectUserInputForm,
TextUserInputForm,
UserInputControlType
} from './types.js'
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load configuration
const config = loadConfig(process.env.CONFIG_PATH || path.resolve(__dirname, '../config.yaml'))
// Create server instance
const server = new Server(
{
name: 'dify-workflow-mcp',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
)
// cache dify parameters
const difyParametersMap = new Map<string, DifyParameters>()
// cache name app sks
const appSkMap = new Map<string, string>()
// Initialize Dify clients
const difyClients = new Map<string, DifyClient>()
for (const appSk of config.dify_app_sks) {
const client = new DifyClient(config.dify_base_url, appSk)
difyClients.set(appSk, client)
}
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: MCPTool[] = []
let index = 0
for (const client of difyClients.values()) {
try {
const [appInfo, parameters] = await Promise.all([client.getAppInfo(), client.getParameters()])
const inputSchema: MCPToolInputSchema = convertDifyParametersToJsonSchema(parameters)
// Cache Dify parameters
difyParametersMap.set(appInfo.name, parameters)
// Cache app sk
appSkMap.set(appInfo.name, config.dify_app_sks[index++])
tools.push({
name: appInfo.name,
description: appInfo.description,
inputSchema
})
} catch (error) {
console.error('Failed to load tool:', error)
}
}
return { tools }
})
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
try {
// Find the corresponding Dify client
const appSk = appSkMap.get(name)
if (!appSk) {
throw new Error('Unsupported tool')
}
const client = difyClients.get(appSk)
if (!client) {
throw new Error('No Dify client available')
}
const difyParameters = difyParametersMap.get(name)
if (!difyParameters) {
throw new Error('No Dify parameters available')
}
// Validate input parameters
const validatedArgs = await validateInputParameters(args, difyParameters)
// Execute the workflow
const result = await client.runWorkflow(validatedArgs)
return {
content: [
{
type: 'text',
text: result
}
]
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid arguments: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`)
}
throw error
}
})
// Validate input parameters
const validateInputParameters = (args: any, difyParameters: DifyParameters) => {
const schema = z.object(
Object.fromEntries(
difyParameters.user_input_form.map((form) => {
if (isParagraphInput(form)) {
const { required, label, variable } = form[UserInputControlType.ParagraphInput]
const currentSchema = required
? z.string({
message: `${label} is required!`
})
: z.optional(z.string())
return [variable, currentSchema]
}
if (isTextInput(form)) {
const { required, label, variable } = form[UserInputControlType.TextInput]
const currentSchema = required
? z.string({
message: `${label} is required!`
})
: z.optional(z.string())
return [variable, currentSchema]
}
if (isSelectInput(form)) {
const { required, options, variable } = form[UserInputControlType.SelectInput]
const currentSchema = required
? z.enum(options as [string, ...string[]])
: z.optional(z.enum(options as [string, ...string[]]))
return [variable, currentSchema]
}
if (isNumberInput(form)) {
const { required, label, variable } = form[UserInputControlType.NumberInput]
const currentSchema = required
? z.number({
message: `${label} is required!`
})
: z.optional(z.number())
return [variable, currentSchema]
}
throw new Error(`Invalid difyParameters`)
})
)
)
return schema.parse(args)
}
/**
* Convert Dify parameters to JSON Schema
*/
const convertDifyParametersToJsonSchema = (parameters: DifyParameters): MCPToolInputSchema => {
const inputSchema: MCPToolInputSchema = {
type: 'object',
properties: {},
required: []
}
for (const input of parameters.user_input_form) {
// 处理 UserInputControlType.TextInput
if (isTextInput(input)) {
inputSchema.properties[input[UserInputControlType.TextInput].variable] = {
type: 'string'
}
}
// 处理 UserInputControlType.ParagraphInput
if (isParagraphInput(input)) {
inputSchema.properties[input[UserInputControlType.ParagraphInput].variable] = {
type: 'string'
}
}
// 处理 UserInputControlType.SelectInput
if (isSelectInput(input)) {
inputSchema.properties[input[UserInputControlType.SelectInput].variable] = {
type: 'array',
enum: input[UserInputControlType.SelectInput].options
}
}
// 处理 UserInputControlType.NumberInput
if (isNumberInput(input)) {
inputSchema.properties[input[UserInputControlType.NumberInput].variable] = {
type: 'number'
}
}
}
return inputSchema
}
const isTextInput = (input: BaseUserInputForm): input is TextUserInputForm => {
return input['text'] !== undefined
}
const isParagraphInput = (input: BaseUserInputForm): input is ParagraphUserInputForm => {
return input['paragraph'] !== undefined
}
const isSelectInput = (input: BaseUserInputForm): input is SelectUserInputForm => {
return input['select'] !== undefined
}
const isNumberInput = (input: BaseUserInputForm): input is NumberUserInputForm => {
return input['number'] !== undefined
}
// Start the server
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
console.error('Dify MCP Server running on stdio')
}
main().catch((error) => {
console.error('Fatal error in main():', error)
process.exit(1)
})
```