Skip to main content
Glama
cheungxin

JianDaoYun MCP Server

by cheungxin
index.ts34.4 kB
#!/usr/bin/env node import 'dotenv/config'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { JianDaoYunClient } from './client.js'; import { FieldMatcher } from './field-matcher.js'; import { FormData } from './types.js'; import axios from 'axios'; const server = new Server( { name: 'jiandaoyun-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); let jdyClient: JianDaoYunClient | null = null; // 应用缓存,避免重复API调用 let appListCache: any[] | null = null; let appListCacheTime: number = 0; const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 // 环境变量将在运行时检查,支持MCP配置中的env字段 // 不再在启动时强制检查,允许通过MCP服务器配置传递环境变量 // 不再初始化全局客户端,将在每次调用时创建 /** * 获取应用列表(带缓存) */ async function getAppList(appKey: string): Promise<any[]> { const now = Date.now(); if (appListCache && (now - appListCacheTime) < CACHE_DURATION) { return appListCache; } try { const response = await axios.post( `${process.env.JIANDAOYUN_BASE_URL || 'https://api.jiandaoyun.com'}/api/v5/app/list`, {}, { headers: { 'Authorization': `Bearer ${appKey}`, 'Content-Type': 'application/json' } } ); // 简道云API返回格式通常是 { code: 0, data: [...] } const apps = response.data?.data || response.data || []; appListCache = Array.isArray(apps) ? apps : []; appListCacheTime = now; return appListCache; } catch (error) { console.error('Failed to fetch app list:', error); return []; } } /** * 智能解析表单ID - 自动判断输入的是应用ID还是表单ID */ async function resolveFormId(inputId: string, appKey: string): Promise<{ formId: string; appId?: string; suggestions?: string[] }> { // 如果输入看起来像表单ID(通常24位字符),直接返回 if (inputId.length === 24 && /^[a-f0-9]{24}$/i.test(inputId)) { return { formId: inputId }; } // 尝试作为应用ID处理 const appList = await getAppList(appKey); const targetApp = appList.find(app => app.app_id === inputId); if (targetApp) { // 这是一个应用ID,需要获取其下的表单列表 try { const response = await axios.post( `${process.env.JIANDAOYUN_BASE_URL || 'https://api.jiandaoyun.com'}/api/v1/app/${inputId}/entry/list`, {}, { headers: { 'Authorization': `Bearer ${appKey}`, 'Content-Type': 'application/json' } } ); const forms = response.data || []; if (forms.length === 0) { throw new Error(`应用 "${targetApp.name}" 下没有找到可用的表单`); } // 如果只有一个表单,直接返回 if (forms.length === 1) { return { formId: forms[0].entry_id || forms[0]._id, appId: inputId }; } // 多个表单时,返回建议列表 const suggestions = forms.map((form: any) => `${form.name || '未命名表单'} (${form.entry_id || form._id})` ); return { formId: forms[0].entry_id || forms[0]._id, // 默认返回第一个 appId: inputId, suggestions }; } catch (error) { throw new Error(`无法获取应用 "${targetApp.name}" 下的表单列表: ${error instanceof Error ? error.message : '未知错误'}`); } } // 既不是标准表单ID也不是已知应用ID,直接尝试使用 return { formId: inputId }; } /** * 智能字段匹配 - 将用户输入的字段名转换为后台字段名 */ async function smartFieldMapping(formId: string, userData: any, appKey: string, appId?: string): Promise<any> { try { // 获取表单字段信息 const response = await axios.post( `${process.env.JIANDAOYUN_BASE_URL || 'https://api.jiandaoyun.com'}/api/v5/app/entry/widget/list`, { app_id: appId, entry_id: formId }, { headers: { 'Authorization': `Bearer ${appKey}`, 'Content-Type': 'application/json' } } ); // API返回格式: {widgets: [...], sysWidgets: ...} const widgets = response.data?.widgets || []; const mappedData: any = {}; // 为每个用户输入的字段找到对应的后台字段名 for (const [userKey, value] of Object.entries(userData)) { let matchedField = null; // 1. 精确匹配label matchedField = widgets.find((w: any) => w.label === userKey); // 2. 如果没找到,尝试包含匹配 if (!matchedField) { matchedField = widgets.find((w: any) => w.label?.includes(userKey) || userKey.includes(w.label || '') ); } // 3. 如果还没找到,尝试name匹配 if (!matchedField) { matchedField = widgets.find((w: any) => w.name === userKey); } // 4. 常见字段名映射 if (!matchedField) { const commonMappings: { [key: string]: string[] } = { '姓名': ['name', 'username', '用户名', '姓名'], '电话': ['phone', 'tel', 'mobile', '手机', '电话'], '邮箱': ['email', 'mail', '邮件', '邮箱'], '地址': ['address', '地址', '住址'], '备注': ['remark', 'note', 'comment', '备注', '说明'] }; for (const [cnName, enNames] of Object.entries(commonMappings)) { if (userKey === cnName || enNames.includes(userKey)) { matchedField = widgets.find((w: any) => enNames.some(en => w.label?.includes(en) || w.name?.includes(en)) ); if (matchedField) break; } } } if (matchedField) { mappedData[matchedField.name] = value; console.log(`字段映射: "${userKey}" -> "${matchedField.name}" (${matchedField.label})`); } else { // 如果找不到匹配字段,保持原样 mappedData[userKey] = value; console.log(`字段未映射: "${userKey}" 保持原样`); } } return { mappedData, fieldInfo: widgets.map((w: any) => ({ name: w.name, label: w.label, type: w.type, required: w.required })) }; } catch (error) { console.error('字段映射失败:', error); // 如果获取字段信息失败,返回原始数据 return { mappedData: userData, fieldInfo: [] }; } } /** * 增强的错误处理函数 */ function createEnhancedError(error: any, context: string): McpError { let message = `${context}失败`; if (error.response?.status === 403) { message += ': 权限不足,请检查API密钥权限或表单ID是否正确'; } else if (error.response?.status === 400) { const errorData = error.response.data; if (errorData?.msg === 'The form does not exist.') { message += ': 表单不存在,请检查表单ID是否正确'; } else { message += `: ${errorData?.msg || '请求参数错误'}`; } } else if (error.response?.status === 404) { message += ': 资源不存在'; } else { message += `: ${error.message || '未知错误'}`; } return new McpError(ErrorCode.InternalError, message); } server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_form_fields', description: 'Get field definitions for a JianDaoYun form', inputSchema: { type: 'object', properties: { appId: { type: 'string', description: 'The JianDaoYun application ID', }, appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (can be provided via JIANDAOYUN_APP_KEY environment variable)', }, formId: { type: 'string', description: 'The form ID to query fields for (can be form ID or app ID)', }, }, required: ['appId', 'formId'], }, }, { name: 'submit_form_data', description: 'Submit data to a JianDaoYun form with automatic field type matching', inputSchema: { type: 'object', properties: { appId: { type: 'string', description: 'The JianDaoYun application ID', }, appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (optional, will use JIANDAOYUN_APP_KEY from environment if not provided)', }, formId: { type: 'string', description: 'The form ID to submit data to (can be form ID or app ID)', }, data: { type: ['object', 'array'], description: 'The data to submit (single object or array for batch)', }, autoMatch: { type: 'boolean', description: 'Whether to automatically match field types (default: true)', default: true, }, transactionId: { type: 'string', description: 'Optional transaction ID for idempotent submissions', }, }, required: ['appId', 'formId', 'data'], }, }, { name: 'get_form_data', description: 'Get a specific data entry from a JianDaoYun form', inputSchema: { type: 'object', properties: { appId: { type: 'string', description: 'The JianDaoYun application ID', }, appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (optional, will use JIANDAOYUN_APP_KEY from environment if not provided)', }, formId: { type: 'string', description: 'The form ID (can be form ID or app ID)', }, dataId: { type: 'string', description: 'The data entry ID', }, }, required: ['appId', 'formId', 'dataId'], }, }, { name: 'query_form_data', description: 'Query multiple form data entries with filtering support', inputSchema: { type: 'object', properties: { appId: { type: 'string', description: 'The JianDaoYun application ID', }, appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (optional, will use JIANDAOYUN_APP_KEY from environment if not provided)', }, formId: { type: 'string', description: 'The form ID (can be form ID or app ID)', }, dataId: { type: 'string', description: 'Last data ID for pagination', }, fields: { type: 'array', items: { type: 'string' }, description: 'Fields to return (widget IDs)', }, filter: { type: 'object', description: 'Data filter conditions', properties: { rel: { type: 'string', enum: ['and', 'or'], description: 'Relation between conditions', }, cond: { type: 'array', description: 'Filter conditions', items: { type: 'object', properties: { field: { type: 'string' }, type: { type: 'string' }, method: { type: 'string' }, value: {}, }, required: ['field', 'method'], }, }, }, required: ['rel', 'cond'], }, limit: { type: 'number', description: 'Number of records to return (1-100, default: 10)', minimum: 1, maximum: 100, }, }, required: ['appId', 'formId'], }, }, { name: 'update_form_data', description: 'Update an existing form data entry', inputSchema: { type: 'object', properties: { appId: { type: 'string', description: 'The JianDaoYun application ID', }, appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (optional, will use JIANDAOYUN_APP_KEY from environment if not provided)', }, formId: { type: 'string', description: 'The form ID (can be form ID or app ID)', }, dataId: { type: 'string', description: 'The data entry ID to update', }, data: { type: 'object', description: 'The data to update', }, transactionId: { type: 'string', description: 'Optional transaction ID', }, isStartTrigger: { type: 'boolean', description: 'Whether to trigger automation', }, }, required: ['appId', 'formId', 'dataId', 'data'], }, }, { name: 'delete_form_data', description: 'Delete one or more form data entries', inputSchema: { type: 'object', properties: { appId: { type: 'string', description: 'The JianDaoYun application ID', }, appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (optional, will use JIANDAOYUN_APP_KEY from environment if not provided)', }, formId: { type: 'string', description: 'The form ID (can be form ID or app ID)', }, dataIds: { type: ['string', 'array'], description: 'Data ID(s) to delete', items: { type: 'string' }, }, isStartTrigger: { type: 'boolean', description: 'Whether to trigger automation', }, }, required: ['appId', 'formId', 'dataIds'], }, }, { name: 'get_upload_token', description: 'Get file upload tokens for file/image fields', inputSchema: { type: 'object', properties: { appId: { type: 'string', description: 'The JianDaoYun application ID', }, appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (optional, will use JIANDAOYUN_APP_KEY from environment if not provided)', }, formId: { type: 'string', description: 'The form ID (can be form ID or app ID)', }, transactionId: { type: 'string', description: 'Transaction ID to bind uploads to', }, }, required: ['appId', 'formId', 'transactionId'], }, }, { name: 'list_apps_and_forms', description: 'List all available applications and their forms that the current API key can access', inputSchema: { type: 'object', properties: { appKey: { type: 'string', description: 'The JianDaoYun application key (API key) (optional, will use JIANDAOYUN_APP_KEY from environment if not provided)', }, appId: { type: 'string', description: 'Optional: specific app ID to get forms for. If not provided, lists all apps.', }, }, required: [], }, }, ], }; }); /** * 获取参数默认值,appKey从环境变量获取,appId需要用户提供 */ function getDefaultParams(args: any) { return { appId: args.appId, appKey: args.appKey || process.env.JIANDAOYUN_APP_KEY, baseUrl: process.env.JIANDAOYUN_BASE_URL }; } server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_form_fields': { const { formId } = args as { formId: string }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } if (!appId) { throw new Error('appId is required. Please provide it as parameter.'); } try { // 创建客户端实例 const jdyClient = new JianDaoYunClient({ appId, appKey, baseUrl }); const resolved = await resolveFormId(formId, appKey); const fields = await jdyClient.getFormFields(resolved.formId); let responseText = JSON.stringify(fields, null, 2); // 如果有多个表单建议,添加提示信息 if (resolved.suggestions && resolved.suggestions.length > 1) { responseText = `// 注意: 检测到应用下有多个表单,当前使用第一个表单\n// 可用表单列表:\n${resolved.suggestions.map(s => `// - ${s}`).join('\n')}\n\n${responseText}`; } return { content: [ { type: 'text', text: responseText, }, ], }; } catch (error) { throw createEnhancedError(error, '获取表单字段'); } } case 'submit_form_data': { const { formId, data, autoMatch = true, transactionId } = args as { formId: string; data: FormData | FormData[]; autoMatch?: boolean; transactionId?: string; }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } if (!appId) { throw new Error('appId is required. Please provide it as parameter.'); } // 创建客户端实例 const jdyClient = new JianDaoYunClient({ appId, appKey, baseUrl }); let resolved: any; let processedData = data; let fieldMappingInfo = null; let submitResult: any; let errorDetails: any = null; try { resolved = await resolveFormId(formId, appKey); } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: true, message: `表单ID解析失败: ${error instanceof Error ? error.message : String(error)}`, formUsed: null, appId: appId, originalData: data, processedData: null }, null, 2), }, ], }; } if (autoMatch) { try { // 使用智能字段映射 if (Array.isArray(data)) { const mappedResults = []; for (const item of data) { const mappingResult = await smartFieldMapping(resolved.formId, item, appKey, resolved.appId || appId); mappedResults.push(mappingResult.mappedData); if (!fieldMappingInfo) fieldMappingInfo = mappingResult.fieldInfo; } processedData = mappedResults; } else { const mappingResult = await smartFieldMapping(resolved.formId, data, appKey, resolved.appId || appId); processedData = mappingResult.mappedData; fieldMappingInfo = mappingResult.fieldInfo; } } catch (error) { console.log('字段映射失败,使用原始数据:', error instanceof Error ? error.message : String(error)); } } try { submitResult = await jdyClient.submitData({ formId: resolved.formId, data: processedData, transactionId, }); let message = `成功提交 ${Array.isArray(data) ? data.length : 1} 条记录`; if (resolved.suggestions && resolved.suggestions.length > 1) { message += `\n注意: 检测到应用下有多个表单,已使用第一个表单进行提交`; } return { content: [ { type: 'text', text: JSON.stringify({ success: true, result: submitResult, message, formUsed: resolved.formId, appId: resolved.appId || appId, originalData: data, processedData, fieldMapping: fieldMappingInfo }, null, 2), }, ], }; } catch (error) { // 返回详细的错误信息而不是抛出错误 errorDetails = { success: false, error: true, message: '提交表单数据失败', formUsed: resolved?.formId || null, appId: appId, originalData: data, processedData: processedData }; if (error && typeof error === 'object' && 'response' in error && (error as any).response?.data) { // 简道云API错误 const apiError = (error as any).response.data; errorDetails.apiError = { code: apiError.code, message: apiError.msg, details: apiError }; errorDetails.message = `API错误 ${apiError.code}: ${apiError.msg}`; // 根据错误代码提供更详细的说明 if (apiError.code === 3005) { errorDetails.suggestion = '请求参数无效,请检查表单ID、字段名称和数据格式是否正确'; } else if (apiError.code === 3000) { errorDetails.suggestion = '表单不存在,请检查表单ID是否正确'; } else if (apiError.code === 4000) { errorDetails.suggestion = '数据提交失败,请检查字段值是否符合表单要求'; } } else if (error instanceof Error) { errorDetails.message = error.message; } return { content: [ { type: 'text', text: JSON.stringify(errorDetails, null, 2), }, ], }; } } case 'get_form_data': { const { formId, dataId } = args as { formId: string; dataId: string }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } if (!appId) { throw new Error('appId is required. Please provide it as parameter.'); } try { // 创建客户端实例 const jdyClient = new JianDaoYunClient({ appId, appKey, baseUrl }); const resolved = await resolveFormId(formId, appKey); const data = await jdyClient.getFormData(resolved.formId, dataId); return { content: [ { type: 'text', text: JSON.stringify({ data, formUsed: resolved.formId, appId: resolved.appId || appId }, null, 2), }, ], }; } catch (error) { throw createEnhancedError(error, '获取表单数据'); } } case 'query_form_data': { const { formId, dataId, fields, filter, limit } = args as { formId: string; dataId?: string; fields?: string[]; filter?: any; limit?: number; }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } if (!appId) { throw new Error('appId is required. Please provide it as parameter.'); } try { // 创建客户端实例 const jdyClient = new JianDaoYunClient({ appId, appKey, baseUrl }); const resolved = await resolveFormId(formId, appKey); const result = await jdyClient.queryFormData({ formId: resolved.formId, dataId, fields, filter, limit, }); return { content: [ { type: 'text', text: JSON.stringify({ ...result, formUsed: resolved.formId, appId: resolved.appId || appId }, null, 2), }, ], }; } catch (error) { throw createEnhancedError(error, '查询表单数据'); } } case 'update_form_data': { const { formId, dataId, data, transactionId, isStartTrigger } = args as { formId: string; dataId: string; data: FormData; transactionId?: string; isStartTrigger?: boolean; }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } if (!appId) { throw new Error('appId is required. Please provide it as parameter.'); } try { // 创建客户端实例 const jdyClient = new JianDaoYunClient({ appId, appKey, baseUrl }); const resolved = await resolveFormId(formId, appKey); const result = await jdyClient.updateFormData(resolved.formId, dataId, data, { transactionId, isStartTrigger, }); return { content: [ { type: 'text', text: JSON.stringify({ success: true, result, message: '数据更新成功', formUsed: resolved.formId, appId: resolved.appId || appId }, null, 2), }, ], }; } catch (error) { throw createEnhancedError(error, '更新表单数据'); } } case 'delete_form_data': { const { formId, dataIds, isStartTrigger } = args as { formId: string; dataIds: string | string[]; isStartTrigger?: boolean; }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } if (!appId) { throw new Error('appId is required. Please provide it as parameter.'); } try { // 创建客户端实例 const jdyClient = new JianDaoYunClient({ appId, appKey, baseUrl }); const resolved = await resolveFormId(formId, appKey); const result = await jdyClient.deleteFormData(resolved.formId, dataIds, { isStartTrigger, }); return { content: [ { type: 'text', text: JSON.stringify({ success: true, result, message: `成功删除 ${Array.isArray(dataIds) ? dataIds.length : 1} 条记录`, formUsed: resolved.formId, appId: resolved.appId || appId }, null, 2), }, ], }; } catch (error) { throw createEnhancedError(error, '删除表单数据'); } } case 'get_upload_token': { const { formId, transactionId } = args as { formId: string; transactionId: string; }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } if (!appId) { throw new Error('appId is required. Please provide it as parameter.'); } try { // 创建客户端实例 const jdyClient = new JianDaoYunClient({ appId, appKey, baseUrl }); const resolved = await resolveFormId(formId, appKey); const result = await jdyClient.getUploadToken(resolved.formId, transactionId); return { content: [ { type: 'text', text: JSON.stringify({ success: true, result, formUsed: resolved.formId, appId: resolved.appId || appId }, null, 2), }, ], }; } catch (error) { throw createEnhancedError(error, '获取上传令牌'); } } case 'list_apps_and_forms': { const { appId: specificAppId } = args as { appId?: string }; const { appId, appKey, baseUrl } = getDefaultParams(args); // 验证必需参数 if (!appKey) { throw new Error('appKey is required. Please set JIANDAOYUN_APP_KEY in MCP server configuration.'); } try { const targetAppId = specificAppId || appId; if (targetAppId) { // 获取特定应用的表单列表 const jdyClient = new JianDaoYunClient({ appId: targetAppId, appKey, baseUrl }); try { const response = await axios.post( `${baseUrl}/api/v1/app/${targetAppId}/entry/list`, {}, { headers: { 'Authorization': `Bearer ${appKey}`, 'Content-Type': 'application/json' } } ); const forms = response.data || []; return { content: [ { type: 'text', text: JSON.stringify({ appId: targetAppId, forms: forms.map((form: any) => ({ id: form.entry_id || form._id, name: form.name || '未命名表单', description: form.description || '', created_time: form.created_time, updated_time: form.updated_time })), total: forms.length }, null, 2), }, ], }; } catch (error) { throw new Error(`无法获取应用 "${targetAppId}" 下的表单列表: ${error instanceof Error ? error.message : '未知错误'}`); } } else { // 获取所有应用列表 const apps = await getAppList(appKey); return { content: [ { type: 'text', text: JSON.stringify({ apps: apps.map(app => ({ id: app.app_id, name: app.name, description: app.description || '', created_time: app.created_time, updated_time: app.updated_time })), total: apps.length, message: '使用 list_apps_and_forms 工具并提供 appId 参数可以查看特定应用下的表单列表' }, null, 2), }, ], }; } } catch (error) { throw createEnhancedError(error, '获取应用和表单列表'); } } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${errorMessage}` ); } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('JianDaoYun MCP server started'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cheungxin/jiandaoyun-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server