Dify MCP Server
by faiz-gear
- src
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 { loadConfig } from './config.js'
import { DifyClient } from './dify-client.js'
import {
BaseUserInputForm,
DifyParameters,
MCPTool,
MCPToolInputSchema,
NumberUserInputForm,
ParagraphUserInputForm,
SelectUserInputForm,
TextUserInputForm,
UserInputControlType
} from './types.js'
// Load configuration
const config = loadConfig(process.env.CONFIG_PATH || 'config.yaml')
// Create server instance
const server = new Server(
{
name: 'dify-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-input'] !== undefined
}
const isParagraphInput = (input: BaseUserInputForm): input is ParagraphUserInputForm => {
return input['paragraph-input'] !== undefined
}
const isSelectInput = (input: BaseUserInputForm): input is SelectUserInputForm => {
return input['select-input'] !== 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)
})