Skip to main content
Glama

Kintone MCP Server

by r3-yamauchi
LayoutTools.js55.5 kB
// src/server/tools/LayoutTools.js import { autoCorrectFieldWidth, autoCorrectLayoutWidths } from '../../utils/LayoutUtils.js'; import { ValidationUtils } from '../../utils/ValidationUtils.js'; import { LoggingUtils } from '../../utils/LoggingUtils.js'; import { ResponseBuilder } from '../../utils/ResponseBuilder.js'; // フィールドコードの自動生成関数 function generateFieldCode(label) { if (!label) return ''; // ラベルから使用可能な文字のみを抽出 let code = label; // 英数字、ひらがな、カタカナ、漢字、許可された記号以外を削除 code = code.replace(/[^a-zA-Z0-9ぁ-んァ-ヶー一-龠々__・・$¥]/g, '_'); // 先頭が数字の場合、先頭に 'f_' を追加 if (/^[0-90-9]/.test(code)) { code = 'f_' + code; } return code; } // システムフィールドとレイアウト要素のリスト(事前作成不要) const SYSTEM_FIELD_TYPES = [ "RECORD_NUMBER", "CREATOR", "MODIFIER", "CREATED_TIME", "UPDATED_TIME" ]; // レイアウト要素のリスト(事前作成不要) const LAYOUT_ELEMENT_TYPES = [ "LABEL", "SPACER", "HR" ]; // レイアウトデータを再帰的に検証・修正する関数(同期版) function validateAndFixLayout(layout, existingFieldCodes = []) { // 使用済みフィールドコードのリスト(既存 + 新規追加済み) const usedFieldCodes = [...existingFieldCodes]; if (!Array.isArray(layout)) { LoggingUtils.logWarning('validateAndFixLayout', 'レイアウトが配列ではありません。自動的に配列に変換します。'); layout = [layout]; } // ROW内やGROUP内から抽出したSUBTABLEやGROUPを保存するための配列 const extractedElements = []; // 各レイアウト要素を検証・修正 const processedLayout = layout.map(item => { // typeプロパティが指定されていない場合は自動的に補完 if (!item.type) { // トップレベルの要素は ROW, GROUP, SUBTABLE のいずれかである必要がある item.type = "ROW"; // デフォルトは ROW LoggingUtils.logWarning('validateAndFixLayout', 'レイアウト要素に type プロパティが指定されていません。自動的に "ROW" を設定します。'); } // 要素タイプに応じた検証・修正 if (item.type === "ROW") { // fieldsプロパティが指定されていない場合は自動的に補完 if (!item.fields) { item.fields = []; LoggingUtils.logWarning('validateAndFixLayout', 'ROW要素に fields プロパティが指定されていません。空の配列を設定します。'); } // fieldsプロパティが配列でない場合は配列に変換 if (!Array.isArray(item.fields)) { LoggingUtils.logWarning('validateAndFixLayout', 'ROW要素の fields プロパティが配列ではありません。自動的に配列に変換します。'); item.fields = [item.fields]; } // ROW要素内からGROUP要素を抽出してトップレベルに移動 const groupFields = item.fields.filter(field => field.type === "GROUP"); if (groupFields.length > 0) { LoggingUtils.logWarning('validateAndFixLayout', 'ROW要素内のGROUP要素を自動的にトップレベルに移動しました。kintoneの仕様により、グループフィールドはトップレベルに配置する必要があります。'); // GROUP要素をトップレベルに移動するために保存 extractedElements.push(...groupFields); // ROW内からは除外 item.fields = item.fields.filter(field => field.type !== "GROUP"); } // ROW要素内からSUBTABLE要素を抽出してトップレベルに移動 const subtableFields = item.fields.filter(field => field.type === "SUBTABLE"); if (subtableFields.length > 0) { LoggingUtils.logWarning('validateAndFixLayout', 'ROW要素内のSUBTABLE要素を自動的にトップレベルに移動しました。kintoneの仕様により、テーブルはトップレベルに配置する必要があります。'); // SUBTABLE要素をトップレベルに移動するために保存 extractedElements.push(...subtableFields); // ROW内からは除外 item.fields = item.fields.filter(field => field.type !== "SUBTABLE"); } // 各フィールド要素を検証・修正 item.fields = item.fields.map(field => { // typeプロパティが指定されていない場合は自動的に補完 if (!field.type) { // フィールド要素のデフォルトタイプは SINGLE_LINE_TEXT field.type = "SINGLE_LINE_TEXT"; LoggingUtils.logWarning('validateAndFixLayout', 'フィールド要素に type プロパティが指定されていません。自動的に "SINGLE_LINE_TEXT" を設定します。'); } // フィールドコードが存在するかチェック(システムフィールドとレイアウト要素は除外) if (field.code && !existingFieldCodes.includes(field.code) && !SYSTEM_FIELD_TYPES.includes(field.type) && !LAYOUT_ELEMENT_TYPES.includes(field.type) && field.type !== "REFERENCE_TABLE") { LoggingUtils.logWarning('validateAndFixLayout', `フィールドコード "${field.code}" (タイプ: ${field.type}) は存在しません。` + `このフィールドはレイアウトに含める前に add_fields ツールで作成する必要があります。` + `システムフィールド(${SYSTEM_FIELD_TYPES.join(', ')})とレイアウト要素(${LAYOUT_ELEMENT_TYPES.join(', ')})は事前作成不要です。`); } return field; }); } else if (item.type === "GROUP") { // labelプロパティが指定されていない場合は自動的に補完 if (!item.label) { item.label = `グループ${Date.now()}`; LoggingUtils.logWarning('validateAndFixLayout', `GROUP要素に label プロパティが指定されていません。自動的に "${item.label}" を設定します。`); } // codeプロパティが指定されていない場合は自動的に補完 if (!item.code) { // labelから自動生成 item.code = generateFieldCode(item.label); LoggingUtils.logDetailedOperation('validateAndFixLayout', 'GROUP要素のコードを自動生成', { code: item.code }); } // fieldsプロパティが指定されている場合はlayoutプロパティに変換 if (item.fields !== undefined) { LoggingUtils.logWarning('validateAndFixLayout', `GROUP要素 "${item.code}" に fields プロパティが指定されています。layout プロパティに変換します。GROUP要素には fields ではなく layout プロパティを使用してください。`); // fieldsプロパティが配列でない場合は配列に変換 if (!Array.isArray(item.fields)) { item.fields = [item.fields]; } // fieldsの内容をROW要素に変換してlayoutに設定 if (item.fields.length > 0) { item.layout = [{ type: "ROW", fields: item.fields }]; } else { item.layout = []; } // fieldsプロパティを削除 delete item.fields; } // 既存のフィールドコードとの重複チェック if (usedFieldCodes.includes(item.code)) { // 重複する場合、新しいフィールドコードを生成 const originalCode = item.code; let newCode = originalCode; let suffix = 1; // 一意のフィールドコードになるまで接尾辞を追加 while (usedFieldCodes.includes(newCode)) { newCode = `${originalCode}_${suffix}`; suffix++; } // フィールドコードを更新 item.code = newCode; LoggingUtils.logDetailedOperation('validateAndFixLayout', 'GROUP要素のフィールドコード重複を解消', { newCode: item.code }); } // 使用済みリストに追加 usedFieldCodes.push(item.code); // openGroup プロパティが指定されていない場合は true を設定 // kintoneの仕様では省略すると false になるが、このMCP Serverでは明示的に true を設定 if (item.openGroup === undefined) { item.openGroup = true; LoggingUtils.logDetailedOperation('validateAndFixLayout', 'GROUP要素のopenGroupを自動設定', { code: item.code, openGroup: true }); } // layoutプロパティが指定されていない場合は空の配列を設定 if (item.layout === undefined) { item.layout = []; LoggingUtils.logWarning('validateAndFixLayout', `GROUP要素 "${item.code}" に layout プロパティが指定されていません。空の配列を設定します。`); } // layoutプロパティが配列でない場合は配列に変換 if (!Array.isArray(item.layout)) { LoggingUtils.logWarning('validateAndFixLayout', `GROUP要素 "${item.code}" の layout プロパティが配列ではありません。自動的に配列に変換します。`); item.layout = [item.layout]; } // グループ内からSUBTABLEとGROUP要素を抽出してトップレベルに移動 const filteredLayout = []; for (const subItem of item.layout) { if (subItem.type === "SUBTABLE") { LoggingUtils.logWarning('validateAndFixLayout', `GROUP要素 "${item.code}" 内のSUBTABLE要素を自動的にトップレベルに移動しました。kintoneの仕様により、グループフィールド内にテーブルを入れることはできません。`); extractedElements.push(subItem); } else if (subItem.type === "GROUP") { LoggingUtils.logWarning('validateAndFixLayout', `GROUP要素 "${item.code}" 内のGROUP要素を自動的にトップレベルに移動しました。kintoneの仕様により、グループフィールド内にグループフィールドを入れることはできません。`); extractedElements.push(subItem); } else { filteredLayout.push(subItem); } } // グループ内の各レイアウト要素を再帰的に検証・修正(同期的に呼び出し) if (filteredLayout.length > 0) { item.layout = validateAndFixLayout(filteredLayout); } else { item.layout = []; } } else if (item.type === "SUBTABLE") { // labelプロパティが指定されていない場合は自動的に補完 if (!item.label) { item.label = `テーブル${Date.now()}`; LoggingUtils.logWarning('validateAndFixLayout', `SUBTABLE要素に label プロパティが指定されていません。自動的に "${item.label}" を設定します。`); } // codeプロパティが指定されていない場合は自動的に補完 if (!item.code) { // labelから自動生成 item.code = generateFieldCode(item.label); LoggingUtils.logDetailedOperation('validateAndFixLayout', 'SUBTABLE要素のコードを自動生成', { code: item.code }); } // 既存のフィールドコードとの重複チェック if (usedFieldCodes.includes(item.code)) { // 重複する場合、新しいフィールドコードを生成 const originalCode = item.code; let newCode = originalCode; let suffix = 1; // 一意のフィールドコードになるまで接尾辞を追加 while (usedFieldCodes.includes(newCode)) { newCode = `${originalCode}_${suffix}`; suffix++; } // フィールドコードを更新 item.code = newCode; LoggingUtils.logDetailedOperation('validateAndFixLayout', 'SUBTABLE要素のフィールドコード重複を解消', { newCode: item.code }); } // 使用済みリストに追加 usedFieldCodes.push(item.code); // テーブル内のフィールドを検証(テーブルのフィールド定義を取得できる場合) if (item.fields) { // GROUP要素がテーブル内に含まれていないことを確認 const groupFields = Object.entries(item.fields).filter(([_, field]) => field.type === "GROUP"); if (groupFields.length > 0) { LoggingUtils.logWarning('validateAndFixLayout', 'SUBTABLE要素内にGROUP要素が含まれています。kintoneの仕槕により、グループフィールドはテーブル化できません。GROUP要素を自動的に除外します。'); // GROUP要素を除外 groupFields.forEach(([key, _]) => { delete item.fields[key]; }); } } } return item; }); // 抽出した要素をトップレベルに追加 return [...processedLayout, ...extractedElements]; } // スペース要素を作成する関数 function createSpacerElement(elementId = null, size = null) { const element = { type: "SPACER", elementId: elementId || `spacer_${Date.now()}` }; if (size) { element.size = size; } return element; } // 罫線要素を作成する関数 function createHrElement(elementId = null) { return { type: "HR", elementId: elementId || `hr_${Date.now()}` }; } // ラベル要素を作成する関数 function createLabelElement(value, elementId = null) { return { type: "LABEL", value: value, elementId: elementId || `label_${Date.now()}` }; } // GROUPフィールドのlabelプロパティを削除し、layoutが空配列の場合はlayoutプロパティ自体を削除する関数 function cleanupGroupElements(layout) { if (!Array.isArray(layout)) { return layout; } return layout.map(item => { if (item.type === "GROUP") { // labelプロパティを削除 if (item.label !== undefined) { delete item.label; } // layoutプロパティが空配列の場合は削除 if (Array.isArray(item.layout) && item.layout.length === 0) { delete item.layout; } else if (Array.isArray(item.layout)) { // layoutプロパティが配列の場合は再帰的に処理 item.layout = cleanupGroupElements(item.layout); } } else if (item.type === "ROW" && Array.isArray(item.fields)) { // ROW要素内のGROUP要素も処理 item.fields = item.fields.map(field => { if (field.type === "GROUP") { // labelプロパティを削除 if (field.label !== undefined) { delete field.label; } // layoutプロパティが空配列の場合は削除 if (Array.isArray(field.layout) && field.layout.length === 0) { delete field.layout; } else if (Array.isArray(field.layout)) { // layoutプロパティが配列の場合は再帰的に処理 field.layout = cleanupGroupElements(field.layout); } } return field; }); } return item; }); } // レイアウト関連のツールを処理する関数 export async function handleLayoutTools(name, args, repository) { // 共通のツール実行ログ LoggingUtils.logToolExecution('layout', name, args); switch (name) { case 'get_form_layout': { ValidationUtils.validateRequired(args, ['app_id']); LoggingUtils.logDetailedOperation('get_form_layout', 'フォームレイアウト取得', { appId: args.app_id }); const response = await repository.getFormLayout(args.app_id); return response; } case 'update_form_layout': { ValidationUtils.validateRequired(args, ['app_id', 'layout']); ValidationUtils.validateArray(args.layout, 'layout'); LoggingUtils.logDetailedOperation('update_form_layout', 'フォームレイアウト更新開始', { appId: args.app_id }); LoggingUtils.logDetailedOperation('update_form_layout', '入力レイアウト', { layout: args.layout }); // 既存のフィールド情報を取得 let existingFieldCodes = []; try { const existingFields = await repository.getFormFields(args.app_id); existingFieldCodes = Object.keys(existingFields.properties || {}); LoggingUtils.logDetailedOperation('update_form_layout', '既存フィールド一覧', { fieldCodes: existingFieldCodes }); } catch (error) { LoggingUtils.logWarning('update_form_layout', `既存フィールドの取得に失敗: ${error.message}`); LoggingUtils.logWarning('update_form_layout', '重複チェックなしで続行します'); } // レイアウトデータを検証・修正(同期的に呼び出し) const validatedLayout = validateAndFixLayout(args.layout, existingFieldCodes); // 変換後のレイアウトをログに出力 LoggingUtils.logDetailedOperation('update_form_layout', '検証済みレイアウト', { layout: validatedLayout }); // フォームフィールド情報を取得 let formFields = null; try { LoggingUtils.logDetailedOperation('update_form_layout', 'フォームフィールド情報取得中', { appId: args.app_id }); const fieldsResponse = await repository.getFormFields(args.app_id); formFields = fieldsResponse.properties || {}; LoggingUtils.logDetailedOperation('update_form_layout', 'フォームフィールド情報取得完了', { fieldCount: Object.keys(formFields).length }); // ルックアップフィールドの情報をログに出力(デバッグ用) const lookupFields = Object.entries(formFields).filter(([_, field]) => field.lookup !== undefined); if (lookupFields.length > 0) { LoggingUtils.logDetailedOperation('update_form_layout', 'ルックアップフィールド検出', { count: lookupFields.length }); lookupFields.forEach(([code, field]) => { LoggingUtils.logDetailedOperation('update_form_layout', 'ルックアップフィールド詳細', { code, type: field.type, relatedApp: field.lookup.relatedApp?.app, relatedKeyField: field.lookup.relatedKeyField }); }); } else { LoggingUtils.logDetailedOperation('update_form_layout', 'ルックアップフィールドなし', {}); } } catch (error) { LoggingUtils.logWarning('update_form_layout', `フォームフィールド情報の取得に失敗: ${error.message}`); LoggingUtils.logWarning('update_form_layout', '幅の自動補正をスキップします'); formFields = null; } // レイアウトの幅を自動補正 let correctedLayout = validatedLayout; let layoutGuidances = []; if (formFields) { LoggingUtils.logDetailedOperation('update_form_layout', 'レイアウト幅の自動補正開始', {}); // 補正前のレイアウト情報をログに出力 const fieldsBeforeCorrection = []; const extractFieldInfo = (items) => { items.forEach(item => { if (item.type === "ROW" && item.fields) { item.fields.forEach(field => { if (field.code) { fieldsBeforeCorrection.push({ code: field.code, type: field.type, width: field.size?.width || "未指定", isLookup: formFields[field.code]?.lookup !== undefined }); } }); } else if (item.type === "GROUP" && item.layout) { extractFieldInfo(item.layout); } }); }; extractFieldInfo(validatedLayout); LoggingUtils.logDetailedOperation('update_form_layout', '補正前のフィールド情報', { fields: fieldsBeforeCorrection }); // 幅の自動補正を実行 const correctionResult = autoCorrectLayoutWidths(validatedLayout, formFields); correctedLayout = correctionResult.layout; layoutGuidances = correctionResult.guidances; LoggingUtils.logDetailedOperation('update_form_layout', 'レイアウト幅の自動補正完了', {}); // ガイダンスメッセージがあれば出力 if (layoutGuidances.length > 0) { LoggingUtils.logDetailedOperation('update_form_layout', 'ガイダンスメッセージ', { messages: layoutGuidances }); } // 補正後のレイアウト情報をログに出力 const fieldsAfterCorrection = []; const extractCorrectedFieldInfo = (items) => { items.forEach(item => { if (item.type === "ROW" && item.fields) { item.fields.forEach(field => { if (field.code) { fieldsAfterCorrection.push({ code: field.code, type: field.type, width: field.size?.width || "未指定", isLookup: formFields[field.code]?.lookup !== undefined }); } }); } else if (item.type === "GROUP" && item.layout) { extractCorrectedFieldInfo(item.layout); } }); }; extractCorrectedFieldInfo(correctedLayout); LoggingUtils.logDetailedOperation('update_form_layout', '補正後のフィールド情報', { fields: fieldsAfterCorrection }); // 変更があったフィールドを特定 const changedFields = fieldsAfterCorrection.filter((field, index) => { const beforeField = fieldsBeforeCorrection[index]; return beforeField && field.width !== beforeField.width; }); if (changedFields.length > 0) { LoggingUtils.logDetailedOperation('update_form_layout', '幅が変更されたフィールド', { fields: changedFields }); } else { LoggingUtils.logDetailedOperation('update_form_layout', '幅が変更されたフィールドなし', {}); } } else { LoggingUtils.logWarning('update_form_layout', 'フォームフィールド情報が取得できなかったため、幅の自動補正をスキップします'); } // GROUPフィールドのlabelプロパティを削除し、layoutが空配列の場合はlayoutプロパティ自体を削除 const cleanedLayout = cleanupGroupElements(correctedLayout); // 深いコピーを作成して参照の問題を解決 const finalLayout = JSON.parse(JSON.stringify(cleanedLayout)); // 最終的なレイアウトをログに出力 LoggingUtils.logDetailedOperation('update_form_layout', '最終レイアウト(API呼び出し前)', { layout: finalLayout }); const revision = args.revision || -1; // リビジョン番号(省略時は最新) try { const response = await repository.updateFormLayout( args.app_id, finalLayout, revision ); // ガイダンスメッセージがあればレスポンスに含める if (layoutGuidances.length > 0) { return { ...response, guidances: layoutGuidances }; } return response; } catch (error) { // エラーの詳細情報を出力 LoggingUtils.logError('update_form_layout', 'フォームレイアウトの更新エラー', error); if (error.errors) { LoggingUtils.logError('update_form_layout', '詳細エラー', error.errors); } throw error; } } case 'create_form_layout': { ValidationUtils.validateRequired(args, ['app_id', 'fields']); ValidationUtils.validateArray(args.fields, 'fields'); LoggingUtils.logDetailedOperation('create_form_layout', 'フォームレイアウト作成開始', { appId: args.app_id }); LoggingUtils.logDetailedOperation('create_form_layout', 'フィールド定義', { fields: args.fields }); // 既存のフィールド情報を取得 let existingFieldCodes = []; try { const existingFields = await repository.getFormFields(args.app_id); existingFieldCodes = Object.keys(existingFields.properties || {}); LoggingUtils.logDetailedOperation('update_form_layout', '既存フィールド一覧', { fieldCodes: existingFieldCodes }); } catch (error) { LoggingUtils.logWarning('update_form_layout', `既存フィールドの取得に失敗: ${error.message}`); LoggingUtils.logWarning('update_form_layout', '重複チェックなしで続行します'); } // レイアウト構造を構築 const layout = buildFormLayout(args.fields, args.options || {}); // レイアウトを検証・修正(同期的に呼び出し) const validatedLayout = validateAndFixLayout(layout, existingFieldCodes); // フォームフィールド情報を取得 let formFields = null; try { const fieldsResponse = await repository.getFormFields(args.app_id); formFields = fieldsResponse.properties || {}; LoggingUtils.logDetailedOperation('create_form_layout', '幅補正用フォームフィールド取得完了', { fieldCount: Object.keys(formFields).length }); // ルックアップフィールドの情報をログに出力(デバッグ用) const lookupFields = Object.entries(formFields).filter(([_, field]) => field.lookup !== undefined); if (lookupFields.length > 0) { LoggingUtils.logDetailedOperation('create_form_layout', 'ルックアップフィールド検出', { count: lookupFields.length, fields: lookupFields.map(([code]) => code) }); } else { LoggingUtils.logDetailedOperation('create_form_layout', 'ルックアップフィールドなし', {}); } } catch (error) { LoggingUtils.logWarning('create_form_layout', `幅補正用フィールド情報の取得に失敗: ${error.message}`); LoggingUtils.logWarning('create_form_layout', '幅補正なしで続行します'); } // レイアウトの幅を自動補正 let correctedLayout = validatedLayout; let layoutGuidances = []; if (formFields) { const correctionResult = autoCorrectLayoutWidths(validatedLayout, formFields); correctedLayout = correctionResult.layout; layoutGuidances = correctionResult.guidances; LoggingUtils.logDetailedOperation('create_form_layout', 'レイアウト幅補正適用完了', {}); // ガイダンスメッセージがあれば出力 if (layoutGuidances.length > 0) { LoggingUtils.logDetailedOperation('update_form_layout', 'ガイダンスメッセージ', { messages: layoutGuidances }); } } // 深いコピーを作成して参照の問題を解決 const finalLayout = JSON.parse(JSON.stringify(correctedLayout)); // 最終的なレイアウトをログに出力 LoggingUtils.logDetailedOperation('update_form_layout', '最終レイアウト(API呼び出し前)', { layout: finalLayout }); try { // レイアウトを更新 const response = await repository.updateFormLayout( args.app_id, finalLayout, -1 // 最新リビジョン ); // ガイダンスメッセージがあればレスポンスに含める if (layoutGuidances.length > 0) { return { revision: response.revision, layout: layout, guidances: layoutGuidances }; } else { return { revision: response.revision, layout: layout }; } } catch (error) { // エラーの詳細情報を出力 LoggingUtils.logError('create_form_layout', 'フォームレイアウトの作成エラー', error); if (error.errors) { LoggingUtils.logError('create_form_layout', '詳細エラー', error.errors); } throw error; } } case 'add_layout_element': { ValidationUtils.validateRequired(args, ['app_id', 'element']); ValidationUtils.validateObject(args.element, 'element'); if (args.position !== undefined) { ValidationUtils.validateObject(args.position, 'position'); } LoggingUtils.logDetailedOperation('add_layout_element', 'レイアウト要素追加開始', { appId: args.app_id }); LoggingUtils.logDetailedOperation('add_layout_element', '追加要素', { element: args.element }); // 現在のレイアウトを取得 const currentLayout = await repository.getFormLayout(args.app_id); // 既存のフィールド情報を取得 let existingFieldCodes = []; try { const existingFields = await repository.getFormFields(args.app_id); existingFieldCodes = Object.keys(existingFields.properties || {}); LoggingUtils.logDetailedOperation('update_form_layout', '既存フィールド一覧', { fieldCodes: existingFieldCodes }); } catch (error) { LoggingUtils.logWarning('update_form_layout', `既存フィールドの取得に失敗: ${error.message}`); LoggingUtils.logWarning('update_form_layout', '重複チェックなしで続行します'); } // 要素を検証・修正 let validatedElement = args.element; if (args.element.type === "GROUP" || args.element.type === "SUBTABLE") { // GROUP要素とSUBTABLE要素は検証・修正(同期的に呼び出し) const validatedElements = validateAndFixLayout([args.element], existingFieldCodes); validatedElement = validatedElements[0]; } // 新しいレイアウトを構築 const newLayout = addElementToLayout( currentLayout.layout, validatedElement, args.position || {} ); // フォームフィールド情報を取得 let formFields = null; try { const fieldsResponse = await repository.getFormFields(args.app_id); formFields = fieldsResponse.properties || {}; LoggingUtils.logDetailedOperation('add_layout_element', '幅補正用フォームフィールド取得完了', {}); } catch (error) { LoggingUtils.logWarning('create_form_layout', `幅補正用フィールド情報の取得に失敗: ${error.message}`); LoggingUtils.logWarning('create_form_layout', '幅補正なしで続行します'); } // レイアウトの幅を自動補正 let correctedLayout = newLayout; let layoutGuidances = []; if (formFields) { const correctionResult = autoCorrectLayoutWidths(newLayout, formFields); correctedLayout = correctionResult.layout; layoutGuidances = correctionResult.guidances; LoggingUtils.logDetailedOperation('create_form_layout', 'レイアウト幅補正適用完了', {}); // ガイダンスメッセージがあれば出力 if (layoutGuidances.length > 0) { LoggingUtils.logDetailedOperation('update_form_layout', 'ガイダンスメッセージ', { messages: layoutGuidances }); } } // 深いコピーを作成して参照の問題を解決 const finalLayout = JSON.parse(JSON.stringify(correctedLayout)); // 最終的なレイアウトをログに出力 LoggingUtils.logDetailedOperation('update_form_layout', '最終レイアウト(API呼び出し前)', { layout: finalLayout }); try { // レイアウトを更新 const response = await repository.updateFormLayout( args.app_id, finalLayout, currentLayout.revision ); // ガイダンスメッセージがあればレスポンスに含める if (layoutGuidances.length > 0) { return { revision: response.revision, layout: newLayout, guidances: layoutGuidances }; } else { return { revision: response.revision, layout: newLayout }; } } catch (error) { // エラーの詳細情報を出力 LoggingUtils.logError('add_layout_element', 'レイアウト要素の追加エラー', error); if (error.errors) { LoggingUtils.logError('add_layout_element', '詳細エラー', error.errors); } throw error; } } case 'create_group_layout': { ValidationUtils.validateRequired(args, ['code', 'label', 'fields']); ValidationUtils.validateArray(args.fields, 'fields'); LoggingUtils.logDetailedOperation('create_group_layout', 'グループレイアウト作成', { code: args.code, label: args.label, fields: args.fields }); // グループ内のレイアウトを構築 const groupLayout = buildGroupLayout(args.fields, args.options || {}); // グループ要素を作成 const groupElement = { type: "GROUP", code: args.code, label: args.label, openGroup: args.openGroup !== false, // デフォルトは開いた状態 layout: groupLayout }; return groupElement; } case 'create_table_layout': { ValidationUtils.validateRequired(args, ['rows']); ValidationUtils.validateArray(args.rows, 'rows'); LoggingUtils.logDetailedOperation('create_table_layout', 'テーブルレイアウト作成', { rowCount: args.rows.length }); // テーブルレイアウトを構築 const tableLayout = buildTableLayout(args.rows, args.options || {}); return tableLayout; } case 'create_spacer_element': { const elementId = args.elementId; const size = {}; if (args.width !== undefined) { size.width = args.width; } if (args.height !== undefined) { size.height = args.height; } const logData = { elementId }; if (Object.keys(size).length > 0) { logData.size = size; } LoggingUtils.logDetailedOperation('create_spacer_element', 'スペーサー要素作成', logData); // スペース要素を作成 const spacerElement = createSpacerElement( elementId, Object.keys(size).length > 0 ? size : null ); return spacerElement; } case 'create_hr_element': { const elementId = args.elementId; LoggingUtils.logDetailedOperation('create_hr_element', '罫線要素作成', { elementId }); // 罫線要素を作成 const hrElement = createHrElement(elementId); return hrElement; } case 'create_label_element': { ValidationUtils.validateRequired(args, ['value']); const value = args.value; const elementId = args.elementId; LoggingUtils.logDetailedOperation('create_label_element', 'ラベル要素作成', { value, elementId }); // ラベル要素を作成 const labelElement = createLabelElement(value, elementId); return labelElement; } default: throw new Error(`Unknown layout tool: ${name}`); } } // フォームレイアウトを構築する関数 function buildFormLayout(fields, options = {}) { const layout = []; // フィールドをグループ化(セクション分け)するかどうか const groupBySection = options.groupBySection === true; // 1行あたりの最大フィールド数 const fieldsPerRow = options.fieldsPerRow || 1; if (groupBySection) { // セクションごとにグループ化 const sections = {}; // フィールドをセクションごとに分類 fields.forEach(field => { const sectionName = field.section || 'デフォルト'; if (!sections[sectionName]) { sections[sectionName] = []; } sections[sectionName].push(field); }); // 各セクションをグループとして追加 Object.entries(sections).forEach(([sectionName, sectionFields]) => { // セクション内のレイアウトを構築 const sectionLayout = buildSectionLayout(sectionFields, { fieldsPerRow }); // セクションがデフォルト以外の場合はグループとして追加 if (sectionName !== 'デフォルト') { layout.push({ type: "GROUP", code: `section_${sectionName.replace(/\s+/g, '_').toLowerCase()}`, label: sectionName, openGroup: true, layout: sectionLayout }); } else { // デフォルトセクションの場合は直接追加 layout.push(...sectionLayout); } }); } else { // セクション分けなしで単純にレイアウトを構築 layout.push(...buildSectionLayout(fields, { fieldsPerRow })); } return layout; } // セクション内のレイアウトを構築する関数 function buildSectionLayout(fields, options = {}) { const layout = []; const fieldsPerRow = options.fieldsPerRow || 1; // フィールドを行ごとに分割 for (let i = 0; i < fields.length; i += fieldsPerRow) { const rowFields = fields.slice(i, i + fieldsPerRow); // 行要素を作成 const row = { type: "ROW", fields: [] }; // 行内の各フィールドを追加 rowFields.forEach(field => { // フィールドタイプに応じた要素を作成 let element; if (field.type === "LABEL") { element = { type: "LABEL", value: field.value || field.label }; } else if (field.type === "SPACER") { element = { type: "SPACER", elementId: field.elementId || `spacer_${Date.now()}` }; } else if (field.type === "HR") { element = { type: "HR", elementId: field.elementId || `hr_${Date.now()}` }; } else if (field.type === "REFERENCE_TABLE") { element = { type: "REFERENCE_TABLE", code: field.code }; } else if (field.type === "GROUP") { // グループ要素はそのまま追加(既に構築済みと仮定) layout.push(field); return; // この要素は行に追加しない } else { // 通常のフィールド - レイアウト要素としては実際のフィールドタイプを使用 // kintoneのAPIではフィールド要素のタイプは実際のフィールドタイプ("NUMBER"など)を指定する必要がある // コードが指定されていない場合はエラー if (!field.code) { throw new Error(`フィールド要素にはコード(code)の指定が必須です。`); } // フィールドタイプの取得(優先順位: field.type > field.fieldType > デフォルト値) let fieldType = field.type || field.fieldType; // フィールドタイプが指定されていない場合はエラー if (!fieldType) { throw new Error(`フィールド要素 "${field.code}" にはフィールドタイプ(type または fieldType)の指定が必須です。`); } element = { type: fieldType, // 実際のフィールドタイプを使用 code: field.code, size: field.size || {} }; } // 行に要素を追加 row.fields.push(element); }); // 行要素をレイアウトに追加(フィールドがある場合のみ) if (row.fields.length > 0) { layout.push(row); } } return layout; } // グループ内のレイアウトを構築する関数 function buildGroupLayout(fields, options = {}) { // 基本的にはセクションレイアウトと同じ return buildSectionLayout(fields, options); } // テーブルレイアウトを構築する関数 function buildTableLayout(rows, options = {}) { const layout = []; // 各行を処理 rows.forEach(rowFields => { // 行要素を作成 const row = { type: "ROW", fields: [] }; // 行内の各フィールドを追加 rowFields.forEach(field => { // フィールドタイプに応じた要素を作成 let element; if (field.type === "LABEL") { element = { type: "LABEL", value: field.value || field.label }; } else if (field.type === "SPACER") { element = { type: "SPACER", elementId: field.elementId || `spacer_${Date.now()}` }; } else if (field.type === "HR") { element = { type: "HR", elementId: field.elementId || `hr_${Date.now()}` }; } else if (field.type === "REFERENCE_TABLE") { element = { type: "REFERENCE_TABLE", code: field.code }; } else { // 通常のフィールド - レイアウト要素としては実際のフィールドタイプを使用 // kintoneのAPIではフィールド要素のタイプは実際のフィールドタイプ("NUMBER"など)を指定する必要がある // コードが指定されていない場合はエラー if (!field.code) { throw new Error(`テーブル内のフィールド要素にはコード(code)の指定が必須です。`); } // フィールドタイプの取得(優先順位: field.type > field.fieldType > デフォルト値) let fieldType = field.type || field.fieldType; // フィールドタイプが指定されていない場合はエラー if (!fieldType) { throw new Error(`テーブル内のフィールド要素 "${field.code}" にはフィールドタイプ(type または fieldType)の指定が必須です。`); } element = { type: fieldType, // 実際のフィールドタイプを使用 code: field.code, size: field.size || {} }; } // 行に要素を追加 row.fields.push(element); }); // 行要素をレイアウトに追加(フィールドがある場合のみ) if (row.fields.length > 0) { layout.push(row); } }); return layout; } // レイアウトに要素を追加する関数 function addElementToLayout(layout, element, position = {}) { // レイアウトのコピーを作成 const newLayout = JSON.parse(JSON.stringify(layout)); // 位置指定がある場合 if (position.index !== undefined) { // 指定された位置に要素を挿入 if (position.type === "GROUP" && position.groupCode) { // グループ内に挿入 const groupIndex = newLayout.findIndex(item => item.type === "GROUP" && item.code === position.groupCode ); if (groupIndex >= 0) { if (!newLayout[groupIndex].layout) { newLayout[groupIndex].layout = []; } // グループ内の指定位置に挿入 if (element.type === "ROW") { // ROW要素はそのまま挿入 newLayout[groupIndex].layout.splice(position.index, 0, element); } else { // その他の要素はROWでラップして挿入 const row = { type: "ROW", fields: [element] }; newLayout[groupIndex].layout.splice(position.index, 0, row); } } } else { // トップレベルに挿入 if (element.type === "ROW" || element.type === "GROUP" || element.type === "SUBTABLE") { // ROW, GROUP, SUBTABLE要素はそのまま挿入 newLayout.splice(position.index, 0, element); } else { // その他の要素はROWでラップして挿入 const row = { type: "ROW", fields: [element] }; newLayout.splice(position.index, 0, row); } } } else if (position.after || position.before) { // 特定の要素の前後に挿入 const targetCode = position.after || position.before; let inserted = false; // レイアウト内の各要素を再帰的に検索 function searchAndInsert(items) { if (inserted) return items; return items.map(item => { // 既に挿入済みなら処理しない if (inserted) return item; // GROUP要素の場合は内部レイアウトも検索 if (item.type === "GROUP" && item.layout) { return { ...item, layout: searchAndInsert(item.layout) }; } // ROW要素の場合はフィールドを検索 if (item.type === "ROW" && item.fields) { // フィールド内にターゲットがあるか検索 // 注意: フィールド要素のタイプは実際のフィールドタイプ("NUMBER"など)になっているため、 // 特定のタイプではなく、コードで検索する const fieldIndex = item.fields.findIndex(field => field.code === targetCode ); if (fieldIndex >= 0) { // 要素を挿入する位置を決定 const insertIndex = position.after ? fieldIndex + 1 : fieldIndex; // 要素を挿入 const newFields = [...item.fields]; newFields.splice(insertIndex, 0, element); inserted = true; return { ...item, fields: newFields }; } } return item; }); } // レイアウト内を検索して要素を挿入 const updatedLayout = searchAndInsert(newLayout); // 要素が挿入されなかった場合は最後に追加 if (!inserted) { if (element.type === "ROW" || element.type === "GROUP" || element.type === "SUBTABLE") { // ROW, GROUP, SUBTABLE要素はそのまま追加 updatedLayout.push(element); } else { // その他の要素はROWでラップして追加 const row = { type: "ROW", fields: [element] }; updatedLayout.push(row); } } return updatedLayout; } else { // 位置指定がない場合は最後に追加 if (element.type === "ROW" || element.type === "GROUP" || element.type === "SUBTABLE") { // ROW, GROUP, SUBTABLE要素はそのまま追加 newLayout.push(element); } else { // その他の要素はROWでラップして追加 const row = { type: "ROW", fields: [element] }; newLayout.push(row); } } return newLayout; }

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/r3-yamauchi/kintone-mcp-server'

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