worsoft-frontend-codegen-local-mcp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,7 @@ This MCP generates Worsoft frontend files from a local Markdown design document.
24
24
  - `mainField` when `style=master_child_jump`: required
25
25
  - `childField` when `style=master_child_jump`: required
26
26
  - `relationType` when `style=master_child_jump`: optional
27
+ - `children` when `style=master_child_jump`: optional array of direct child relations; when provided it overrides the legacy single-child fields
27
28
 
28
29
  ## Recommended MCP Arguments
29
30
 
@@ -57,6 +58,33 @@ Master-child:
57
58
  }
58
59
  ```
59
60
 
61
+ Multi direct children on one main form:
62
+
63
+ ```json
64
+ {
65
+ "tableName": "iwm_sys_trade_level",
66
+ "style": "master_child_jump",
67
+ "children": [
68
+ {
69
+ "childTableName": "iwm_sys_trade_level_standard",
70
+ "mainField": "id",
71
+ "childField": "trade_level_id",
72
+ "relationType": "1:N"
73
+ },
74
+ {
75
+ "childTableName": "iwm_sys_trade_level_competency",
76
+ "mainField": "id",
77
+ "childField": "trade_level_id",
78
+ "relationType": "1:N"
79
+ }
80
+ ],
81
+ "designFile": "plugins/sql/SQL 璁捐璇存槑.md",
82
+ "frontendPath": "E:/own-worker-platform/trunk/worsoft-ui",
83
+ "moduleName": "admin/test",
84
+ "writeToDisk": false
85
+ }
86
+ ```
87
+
60
88
  ## Smoke Test
61
89
 
62
90
  PowerShell:
@@ -65,6 +93,7 @@ PowerShell:
65
93
  powershell -ExecutionPolicy Bypass -File plugins/worsoft-codegen-local/smoke_test_mcp.ps1 -Scenario single
66
94
  powershell -ExecutionPolicy Bypass -File plugins/worsoft-codegen-local/smoke_test_mcp.ps1 -Scenario master
67
95
  powershell -ExecutionPolicy Bypass -File plugins/worsoft-codegen-local/smoke_test_mcp.ps1 -Scenario child
96
+ powershell -ExecutionPolicy Bypass -File plugins/worsoft-codegen-local/smoke_test_mcp.ps1 -Scenario multi
68
97
  powershell -ExecutionPolicy Bypass -File plugins/worsoft-codegen-local/smoke_test_mcp.ps1 -Scenario missing_relation
69
98
  ```
70
99
 
@@ -74,6 +103,7 @@ Node:
74
103
  node plugins/worsoft-codegen-local/smoke_test_mcp.js --scenario single
75
104
  node plugins/worsoft-codegen-local/smoke_test_mcp.js --scenario master
76
105
  node plugins/worsoft-codegen-local/smoke_test_mcp.js --scenario child
106
+ node plugins/worsoft-codegen-local/smoke_test_mcp.js --scenario multi
77
107
  node plugins/worsoft-codegen-local/smoke_test_mcp.js --scenario missing_relation
78
108
  ```
79
109
 
@@ -95,9 +125,8 @@ For `master_child_jump`, MCP does not infer relations from the design file anymo
95
125
 
96
126
  The caller must provide:
97
127
 
98
- - `childTableName`
99
- - `mainField`
100
- - `childField`
128
+ - either `children[]`
129
+ - or the legacy single-child fields: `childTableName`, `mainField`, `childField`
101
130
 
102
131
  If these fields are missing, MCP returns `relation_input_required`.
103
132
 
@@ -40,10 +40,10 @@ export function putObj(obj?: object) {
40
40
  });
41
41
  }
42
42
 
43
- export function delChildObj(ids?: object) {
43
+ export function delChildObj(ids?: object, childTableName?: string) {
44
44
  return request({
45
45
  url: '/{{API_PATH}}/child',
46
46
  method: 'delete',
47
- data: ids,
47
+ data: childTableName ? { ids, childTableName } : ids,
48
48
  });
49
49
  }
@@ -9,14 +9,12 @@
9
9
  {{FORM_FIELDS}}
10
10
  </el-row>
11
11
  <el-row :gutter="24">
12
- <sc-form-table v-model="form.{{CHILD_LIST_NAME}}" :addTemplate="childTemp" @delete="deleteChild" placeholder="No data">
13
- {{CHILD_TABLE_COLUMNS}}
14
- </sc-form-table>
12
+ {{CHILD_SECTIONS}}
15
13
  </el-row>
16
14
  </el-form>
17
15
  <div class="dialog-footer" style="text-align: right; margin-top: 18px;">
18
- <el-button @click="handleBack">Cancel</el-button>
19
- <el-button type="primary" @click="onSubmit" :disabled="loading">Confirm</el-button>
16
+ <el-button @click="handleBack">取消</el-button>
17
+ <el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
20
18
  </div>
21
19
  </el-card>
22
20
  </div>
@@ -38,12 +36,10 @@ const detail = ref(false);
38
36
 
39
37
  const form = reactive({
40
38
  {{FORM_DEFAULTS}}
41
- {{CHILD_LIST_NAME}}: [],
39
+ {{CHILD_FORM_LIST_DEFAULTS}}
42
40
  });
43
41
 
44
- const childTemp = reactive({
45
- {{CHILD_TEMP_DEFAULTS}}
46
- });
42
+ {{CHILD_TEMP_DECLARATIONS}}
47
43
 
48
44
  const dataRules = ref({
49
45
  {{FORM_RULES}}
@@ -55,7 +51,7 @@ const get{{CLASS_NAME}}Data = async (id: string) => {
55
51
  const { data } = await getObj({ {{PK_ATTR}}: id });
56
52
  Object.assign(form, data[0] || {});
57
53
  } catch (error) {
58
- useMessage().error('Failed to fetch data');
54
+ useMessage().error('获取数据失败');
59
55
  } finally {
60
56
  loading.value = false;
61
57
  }
@@ -64,11 +60,11 @@ const get{{CLASS_NAME}}Data = async (id: string) => {
64
60
  const resetFormState = () => {
65
61
  Object.assign(form, {
66
62
  {{FORM_DEFAULTS}}
67
- {{CHILD_LIST_NAME}}: [],
63
+ {{CHILD_FORM_LIST_DEFAULTS}}
68
64
  });
69
65
  nextTick(() => {
70
66
  dataFormRef.value?.resetFields();
71
- form.{{CHILD_LIST_NAME}} = [];
67
+ {{CHILD_RESET_LISTS}}
72
68
  });
73
69
  };
74
70
 
@@ -104,22 +100,22 @@ const onSubmit = async () => {
104
100
 
105
101
  try {
106
102
  form.{{PK_ATTR}} ? await putObj(form) : await addObj(form);
107
- useMessage().success(form.{{PK_ATTR}} ? 'Updated successfully' : 'Created successfully');
103
+ useMessage().success(form.{{PK_ATTR}} ? '修改成功' : '添加成功');
108
104
  closeCurrentPage();
109
105
  } catch (err: any) {
110
- useMessage().error(err.msg || 'Submit failed');
106
+ useMessage().error(err.msg || '提交失败');
111
107
  } finally {
112
108
  loading.value = false;
113
109
  }
114
110
  };
115
111
 
116
- const deleteChild = async (obj: { {{CHILD_PK_ATTR}}: string }) => {
117
- if (obj.{{CHILD_PK_ATTR}}) {
112
+ const deleteChild = async (obj: Record<string, any>, childPkAttr: string, childTableName?: string) => {
113
+ if (obj[childPkAttr]) {
118
114
  try {
119
- await delChildObj([obj.{{CHILD_PK_ATTR}}]);
120
- useMessage().success('Deleted successfully');
115
+ await delChildObj([obj[childPkAttr]], childTableName);
116
+ useMessage().success('删除成功');
121
117
  } catch (err: any) {
122
- useMessage().error(err.msg || 'Delete failed');
118
+ useMessage().error(err.msg || '删除失败');
123
119
  }
124
120
  }
125
121
  };
package/mcp_server.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
7
  const SERVER_NAME = 'worsoft-codegen-local';
8
- const SERVER_VERSION = '0.1.2';
8
+ const SERVER_VERSION = '0.1.4';
9
9
  const PROTOCOL_VERSION = '2024-11-05';
10
10
  const TOOL_NAME = 'worsoft_codegen_local_generate_frontend';
11
11
  const TEMPLATE_LIBRARY_ROOT = path.resolve(__dirname, '..', 'template');
@@ -19,6 +19,21 @@ const TOOL_SCHEMA = {
19
19
  designFile: { type: 'string', description: 'Absolute or relative Markdown design file path. Defaults to ../sql/SQL 设计说明.md when omitted.' },
20
20
  tableName: { type: 'string', description: 'Target main table name from the design file.' },
21
21
  style: { type: 'string', enum: Object.keys(STYLE_CATALOG), description: 'Style id from assets/style-catalog.json.' },
22
+ children: {
23
+ type: 'array',
24
+ description: 'Optional direct child-table relations for master_child_jump. When provided, MCP renders all listed direct child tables on the same main form.',
25
+ items: {
26
+ type: 'object',
27
+ properties: {
28
+ childTableName: { type: 'string', description: 'Child table name.' },
29
+ mainField: { type: 'string', description: 'Main table relation field.' },
30
+ childField: { type: 'string', description: 'Child table relation field.' },
31
+ relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
32
+ },
33
+ required: ['childTableName', 'mainField', 'childField'],
34
+ additionalProperties: false,
35
+ },
36
+ },
22
37
  childTableName: { type: 'string', description: 'Child table name. Required when style=master_child_jump.' },
23
38
  mainField: { type: 'string', description: 'Main table relation field. Required when style=master_child_jump.' },
24
39
  childField: { type: 'string', description: 'Child table relation field. Required when style=master_child_jump.' },
@@ -474,6 +489,15 @@ function buildRetryArguments(safeArgs, sourceFile, childTableName) {
474
489
  retryArguments.designFile = sourceFile;
475
490
  }
476
491
 
492
+ if (safeArgs.children && safeArgs.children.length) {
493
+ retryArguments.children = safeArgs.children.map((relation) => ({
494
+ childTableName: relation.childTableName,
495
+ mainField: relation.mainField,
496
+ childField: relation.childField,
497
+ relationType: relation.relationType || '',
498
+ }));
499
+ }
500
+
477
501
  if (childTableName) {
478
502
  retryArguments.childTableName = childTableName;
479
503
  }
@@ -536,13 +560,43 @@ function loadSourceDocument(safeArgs) {
536
560
  };
537
561
  }
538
562
 
539
- function resolveMarkdownChildRelation(sourceDocument, safeArgs) {
563
+ function normalizeChildrenInput(inputChildren) {
564
+ if (inputChildren === undefined || inputChildren === null) {
565
+ return [];
566
+ }
567
+ if (!Array.isArray(inputChildren)) {
568
+ throw new Error('children must be an array');
569
+ }
570
+ return inputChildren.map((item, index) => {
571
+ if (!item || typeof item !== 'object') {
572
+ throw new Error('children[' + index + '] must be an object');
573
+ }
574
+ const childTableName = item.childTableName ? String(item.childTableName) : '';
575
+ const mainField = item.mainField ? String(item.mainField) : '';
576
+ const childField = item.childField ? String(item.childField) : '';
577
+ const relationType = item.relationType ? String(item.relationType) : '';
578
+ const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !({ childTableName, mainField, childField })[field]);
579
+ if (missingFields.length) {
580
+ throw new Error('children[' + index + '] is missing required fields: ' + missingFields.join(', '));
581
+ }
582
+ return { childTableName, mainField, childField, relationType };
583
+ });
584
+ }
585
+
586
+ function resolveMarkdownChildRelations(sourceDocument, safeArgs) {
587
+ if (safeArgs.children && safeArgs.children.length) {
588
+ return safeArgs.children.map((relation) => ({
589
+ mainTableName: safeArgs.tableName,
590
+ childTableName: relation.childTableName,
591
+ mainField: relation.mainField,
592
+ childField: relation.childField,
593
+ relationType: relation.relationType || '',
594
+ }));
595
+ }
596
+
540
597
  const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !safeArgs[field]);
541
598
  if (missingFields.length) {
542
- const message =
543
- 'master_child_jump requires externally provided relation fields. Missing: ' +
544
- missingFields.join(', ') +
545
- '.';
599
+ const message = 'master_child_jump requires either children[] or legacy relation fields. Missing: ' + missingFields.join(', ') + '.';
546
600
  const error = new Error(message);
547
601
  error.details = {
548
602
  type: 'relation_input_required',
@@ -550,11 +604,19 @@ function resolveMarkdownChildRelation(sourceDocument, safeArgs) {
550
604
  tableName: safeArgs.tableName,
551
605
  designFile: sourceDocument.path,
552
606
  message,
553
- requiredFields: ['childTableName', 'mainField', 'childField'],
607
+ requiredFields: ['children[] or childTableName/mainField/childField'],
554
608
  correctionEntry: {
555
- fields: ['childTableName', 'mainField', 'childField'],
609
+ fields: ['children', 'childTableName', 'mainField', 'childField'],
556
610
  example: {
557
611
  ...buildRetryArguments(safeArgs, sourceDocument.path, safeArgs.childTableName || '<child_table_name>'),
612
+ children: [
613
+ {
614
+ childTableName: safeArgs.childTableName || '<child_table_name>',
615
+ mainField: safeArgs.mainField || '<main_field>',
616
+ childField: safeArgs.childField || '<child_field>',
617
+ relationType: safeArgs.relationType || '1:N',
618
+ },
619
+ ],
558
620
  mainField: safeArgs.mainField || '<main_field>',
559
621
  childField: safeArgs.childField || '<child_field>',
560
622
  },
@@ -563,13 +625,15 @@ function resolveMarkdownChildRelation(sourceDocument, safeArgs) {
563
625
  throw error;
564
626
  }
565
627
 
566
- return {
567
- mainTableName: safeArgs.tableName,
568
- childTableName: safeArgs.childTableName,
569
- mainField: safeArgs.mainField,
570
- childField: safeArgs.childField,
571
- relationType: safeArgs.relationType || '',
572
- };
628
+ return [
629
+ {
630
+ mainTableName: safeArgs.tableName,
631
+ childTableName: safeArgs.childTableName,
632
+ mainField: safeArgs.mainField,
633
+ childField: safeArgs.childField,
634
+ relationType: safeArgs.relationType || '',
635
+ },
636
+ ];
573
637
  }
574
638
 
575
639
  function getStylePreset(styleId) {
@@ -601,31 +665,33 @@ function ensureFieldExists(fields, fieldName, tableName, role) {
601
665
  return field;
602
666
  }
603
667
 
604
- function buildChildModel(sourceDocument, safeArgs, mainParsed) {
605
- if (safeArgs.style !== 'master_child_jump') return null;
606
-
607
- const relation = resolveMarkdownChildRelation(sourceDocument, safeArgs);
608
- const childParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, relation.childTableName);
609
- const childFields = normalizeFields(childParsed);
610
- const mainRelationField = ensureFieldExists(mainParsed.fields, relation.mainField, safeArgs.tableName, 'Main relation');
611
- const childRelationField = ensureFieldExists(childParsed.fields, relation.childField, relation.childTableName, 'Child relation');
612
- const childVisibleFields = childFields.filter(
613
- (field) => field.fieldName !== childParsed.pkField.fieldName && !field.isAudit && field.fieldName !== relation.childField
614
- );
615
-
616
- return {
617
- tableName: relation.childTableName,
618
- tableComment: childParsed.tableComment,
619
- className: toPascalCase(relation.childTableName),
620
- functionName: toCamelCase(relation.childTableName),
621
- listName: toCamelCase(relation.childTableName) + 'List',
622
- pk: childParsed.pkField,
623
- fields: childFields,
624
- visibleFields: childVisibleFields,
625
- mainField: mainRelationField,
626
- childField: childRelationField,
627
- relationType: relation.relationType,
628
- };
668
+ function buildChildModels(sourceDocument, safeArgs, mainParsed) {
669
+ if (safeArgs.style !== 'master_child_jump') return [];
670
+
671
+ const relations = resolveMarkdownChildRelations(sourceDocument, safeArgs);
672
+ return relations.map((relation) => {
673
+ const childParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, relation.childTableName);
674
+ const childFields = normalizeFields(childParsed);
675
+ const mainRelationField = ensureFieldExists(mainParsed.fields, relation.mainField, safeArgs.tableName, 'Main relation');
676
+ const childRelationField = ensureFieldExists(childParsed.fields, relation.childField, relation.childTableName, 'Child relation');
677
+ const childVisibleFields = childFields.filter(
678
+ (field) => field.fieldName !== childParsed.pkField.fieldName && !field.isAudit && field.fieldName !== relation.childField
679
+ );
680
+
681
+ return {
682
+ tableName: relation.childTableName,
683
+ tableComment: childParsed.tableComment,
684
+ className: toPascalCase(relation.childTableName),
685
+ functionName: toCamelCase(relation.childTableName),
686
+ listName: toCamelCase(relation.childTableName) + 'List',
687
+ pk: childParsed.pkField,
688
+ fields: childFields,
689
+ visibleFields: childVisibleFields,
690
+ mainField: mainRelationField,
691
+ childField: childRelationField,
692
+ relationType: relation.relationType,
693
+ };
694
+ });
629
695
  }
630
696
 
631
697
  function buildModel(safeArgs) {
@@ -634,8 +700,9 @@ function buildModel(safeArgs) {
634
700
  const fields = normalizeFields(mainParsed);
635
701
  const visibleFields = fields.filter((field) => field.fieldName !== mainParsed.pkField.fieldName && !field.isAudit);
636
702
  const gridFields = visibleFields.slice(0, 8);
637
- const dictTypes = [...new Set(visibleFields.map((field) => field.dictType).filter(Boolean))];
638
- const child = buildChildModel(sourceDocument, safeArgs, mainParsed);
703
+ const children = buildChildModels(sourceDocument, safeArgs, mainParsed);
704
+ const childDictTypes = children.flatMap((child) => child.visibleFields.map((field) => field.dictType).filter(Boolean));
705
+ const dictTypes = [...new Set([...visibleFields.map((field) => field.dictType).filter(Boolean), ...childDictTypes])];
639
706
 
640
707
  return {
641
708
  sourceFile: sourceDocument.path,
@@ -652,7 +719,7 @@ function buildModel(safeArgs) {
652
719
  dictTypes,
653
720
  frontendPath: path.resolve(safeArgs.frontendPath),
654
721
  style: safeArgs.style,
655
- child,
722
+ children,
656
723
  };
657
724
  }
658
725
 
@@ -739,11 +806,26 @@ function renderTableColumn(field) {
739
806
 
740
807
  function renderChildTableColumn(field, childListName) {
741
808
  const label = field.comment.replace(/'/g, "\\'");
809
+ const rules = field.notNull ? ` :rules="[{ required: true, trigger: 'blur' }]"` : '';
810
+
811
+ let control = ` <el-input v-model="row.${field.attrName}" />`;
812
+ if (field.formType === 'select' && field.dictType) {
813
+ control = [
814
+ ` <el-select v-model="row.${field.attrName}" placeholder="请选择${label}" style="width: 100%">`,
815
+ ` <el-option v-for="item in ${field.dictType}" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
816
+ ' </el-select>',
817
+ ].join('\n');
818
+ } else if (field.formType === 'number') {
819
+ const max = field.comment.includes('%') || field.comment.includes('比例') ? ' :max="100"' : '';
820
+ const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
821
+ control = ` <el-input-number v-model="row.${field.attrName}" :min="0"${max}${precision} style="width: 100%" />`;
822
+ }
823
+
742
824
  return [
743
825
  ` <el-table-column label="${label}" prop="${field.attrName}">`,
744
826
  ' <template #default="{ row, $index }">',
745
- ` <el-form-item :prop="\`${childListName}.\${$index}.${field.attrName}\`" :rules="[{ required: true, trigger: 'blur' }]">`,
746
- ` <el-input v-model="row.${field.attrName}" />`,
827
+ ` <el-form-item :prop="\`${childListName}.\${$index}.${field.attrName}\`"${rules}>`,
828
+ control,
747
829
  ' </el-form-item>',
748
830
  ' </template>',
749
831
  ' </el-table-column>',
@@ -776,6 +858,50 @@ function renderChildTempDefaults(childModel) {
776
858
  return childModel.fields.map(renderDefaultLine).join('\n');
777
859
  }
778
860
 
861
+ function renderChildListDefaultLine(childModel) {
862
+ return ` ${childModel.listName}: [],`;
863
+ }
864
+
865
+ function renderChildTempDeclaration(childModel) {
866
+ return [
867
+ `const childTemp${childModel.className} = reactive({`,
868
+ renderChildTempDefaults(childModel),
869
+ '});',
870
+ ].join('\n');
871
+ }
872
+
873
+ function renderChildSection(childModel, childCount) {
874
+ const title = childModel.tableComment.replace(/'/g, "\\'");
875
+ const deleteExpression =
876
+ childCount > 1
877
+ ? `deleteChild(obj, '${childModel.pk.attrName}', '${childModel.tableName}')`
878
+ : `deleteChild(obj, '${childModel.pk.attrName}')`;
879
+
880
+ return [
881
+ ' <el-col :span="24" class="mb20">',
882
+ ` <div class="mb10" style="font-weight: 600;">${title}</div>`,
883
+ ` <sc-form-table v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" @delete="(obj) => ${deleteExpression}" placeholder="暂无数据">`,
884
+ childModel.visibleFields.map((field) => renderChildTableColumn(field, childModel.listName)).join('\n'),
885
+ ' </sc-form-table>',
886
+ ' </el-col>',
887
+ ].join('\n');
888
+ }
889
+
890
+ function renderChildFormListDefaults(children) {
891
+ if (!children.length) return '';
892
+ return children.map(renderChildListDefaultLine).join('\n');
893
+ }
894
+
895
+ function renderChildTempDeclarations(children) {
896
+ if (!children.length) return '';
897
+ return children.map(renderChildTempDeclaration).join('\n\n');
898
+ }
899
+
900
+ function renderChildResetListLines(children) {
901
+ if (!children.length) return '';
902
+ return children.map((childModel) => ` form.${childModel.listName} = [];`).join('\n');
903
+ }
904
+
779
905
  function renderDictImportBlock(dictTypes) {
780
906
  if (!dictTypes.length) return '';
781
907
  return [
@@ -830,12 +956,10 @@ function buildReplacements(model) {
830
956
  OPTIONS_FIELDS: model.visibleFields.map(renderOptionField).join('\n'),
831
957
  FILTER_TYPES: model.visibleFields.map(renderFilterType).filter(Boolean).join('\n'),
832
958
  FORM_RULES: renderFormRules(model.visibleFields),
833
- CHILD_CLASS_NAME: model.child ? model.child.className : '',
834
- CHILD_LIST_NAME: model.child ? model.child.listName : '',
835
- CHILD_PK_ATTR: model.child ? model.child.pk.attrName : '',
836
- CHILD_RELATION_ATTR: model.child ? model.child.childField.attrName : '',
837
- CHILD_TABLE_COLUMNS: model.child ? model.child.visibleFields.map((field) => renderChildTableColumn(field, model.child.listName)).join('\n') : '',
838
- CHILD_TEMP_DEFAULTS: model.child ? renderChildTempDefaults(model.child) : '',
959
+ CHILD_FORM_LIST_DEFAULTS: renderChildFormListDefaults(model.children),
960
+ CHILD_TEMP_DECLARATIONS: renderChildTempDeclarations(model.children),
961
+ CHILD_RESET_LISTS: renderChildResetListLines(model.children),
962
+ CHILD_SECTIONS: model.children.map((childModel) => renderChildSection(childModel, model.children.length)).join('\n'),
839
963
  };
840
964
  }
841
965
 
@@ -881,6 +1005,7 @@ function ensureArguments(input) {
881
1005
  designFile: input.designFile ? String(input.designFile) : null,
882
1006
  tableName: String(input.tableName),
883
1007
  style,
1008
+ children: normalizeChildrenInput(input.children),
884
1009
  childTableName: input.childTableName ? String(input.childTableName) : null,
885
1010
  mainField: input.mainField ? String(input.mainField) : null,
886
1011
  childField: input.childField ? String(input.childField) : null,
@@ -906,6 +1031,16 @@ function maybeWriteFiles(files, writeToDisk, overwrite) {
906
1031
  }
907
1032
 
908
1033
  function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, note) {
1034
+ const relations = model.children.map((childModel) => ({
1035
+ childTableName: childModel.tableName,
1036
+ childTableComment: childModel.tableComment,
1037
+ mainField: childModel.mainField.fieldName,
1038
+ childField: childModel.childField.fieldName,
1039
+ childListName: childModel.listName,
1040
+ relationType: childModel.relationType || '',
1041
+ }));
1042
+ const selectedList = relations.map((relation) => formatRelationCandidate(relation));
1043
+
909
1044
  return {
910
1045
  mode: 'local-template',
911
1046
  style: safeArgs.style,
@@ -919,32 +1054,23 @@ function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, not
919
1054
  writeToDisk: safeArgs.writeToDisk,
920
1055
  selectedTemplates: sharedTemplates,
921
1056
  files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
922
- relation: model.child
923
- ? {
924
- childTableName: model.child.tableName,
925
- childTableComment: model.child.tableComment,
926
- mainField: model.child.mainField.fieldName,
927
- childField: model.child.childField.fieldName,
928
- childListName: model.child.listName,
929
- relationType: model.child.relationType || '',
930
- }
931
- : null,
932
- relationResolution: model.child
1057
+ relation: relations.length === 1 ? relations[0] : null,
1058
+ relations,
1059
+ relationResolution: model.children.length
933
1060
  ? {
934
1061
  status: 'provided',
935
1062
  tableName: safeArgs.tableName,
936
1063
  designFile: model.designFile,
937
1064
  source: 'arguments',
938
- message: 'The relation was provided by the caller. MCP skipped design-doc relation inference and generated files directly.',
939
- selected: formatRelationCandidate({
940
- childTableName: model.child.tableName,
941
- mainField: model.child.mainField.fieldName,
942
- childField: model.child.childField.fieldName,
943
- relationType: model.child.relationType || '',
944
- }),
1065
+ message:
1066
+ model.children.length > 1
1067
+ ? 'Direct child relations were provided by the caller. MCP skipped design-doc relation inference and generated a single main form with multiple child tables.'
1068
+ : 'The relation was provided by the caller. MCP skipped design-doc relation inference and generated files directly.',
1069
+ selected: selectedList.length === 1 ? selectedList[0] : null,
1070
+ selectedList,
945
1071
  correctionEntry: {
946
- fields: ['childTableName', 'mainField', 'childField'],
947
- example: buildRetryArguments(safeArgs, model.designFile, model.child.tableName),
1072
+ fields: ['children', 'childTableName', 'mainField', 'childField'],
1073
+ example: buildRetryArguments(safeArgs, model.designFile, model.children[0].tableName),
948
1074
  },
949
1075
  }
950
1076
  : null,
@@ -953,7 +1079,9 @@ function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, not
953
1079
  visibleFields: model.visibleFields.length,
954
1080
  dictFields: model.visibleFields.filter((field) => field.dictType).map((field) => field.attrName),
955
1081
  skippedAuditFields: model.fields.filter((field) => field.isAudit).map((field) => field.fieldName),
956
- childVisibleFields: model.child ? model.child.visibleFields.length : 0,
1082
+ childCount: model.children.length,
1083
+ childTables: model.children.map((childModel) => childModel.tableName),
1084
+ childVisibleFields: model.children.reduce((sum, childModel) => sum + childModel.visibleFields.length, 0),
957
1085
  },
958
1086
  note,
959
1087
  };
@@ -996,7 +1124,7 @@ async function onMessage(message) {
996
1124
  }
997
1125
 
998
1126
  if (method === 'tools/list') {
999
- writeMessage(successResponse(id, { tools: [{ name: TOOL_NAME, description: 'Generate Worsoft frontend files and menu SQL from a local Markdown design file with style-based local templates. In master_child_jump mode, the caller must provide childTableName, mainField, and childField.', inputSchema: TOOL_SCHEMA }] }));
1127
+ writeMessage(successResponse(id, { tools: [{ name: TOOL_NAME, description: 'Generate Worsoft frontend files and menu SQL from a local Markdown design file with style-based local templates. In master_child_jump mode, the caller must provide either children[] or childTableName/mainField/childField.', inputSchema: TOOL_SCHEMA }] }));
1000
1128
  return;
1001
1129
  }
1002
1130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worsoft-frontend-codegen-local-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Worsoft frontend local-template code generation MCP server.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "worsoft <sw@worsoft.vip>",