#
tokens: 3200/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# 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)
})

```