worsoft-frontend-codegen-local-mcp 0.1.20 → 0.1.21

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/mcp_server.js CHANGED
@@ -5,12 +5,11 @@ 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.20';
8
+ const SERVER_VERSION = '0.1.21';
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');
12
12
  const STYLE_CATALOG_PATH = path.join(__dirname, 'assets', 'style-catalog.json');
13
- const DEFAULT_DESIGN_FILE = path.resolve(__dirname, '..', 'sql', 'SQL 闁荤姳鐒﹀畷姗€顢橀崫銉﹀珰閻庢稒蓱椤?md');
14
13
  const STYLE_CATALOG = loadStyleCatalog();
15
14
  const DEFAULT_DICT_REGISTRY_KEYS = {
16
15
  add_start_stop: 'COMMON_STATUS',
@@ -124,41 +123,87 @@ export function createCrudSchema(
124
123
  }
125
124
  `;
126
125
 
127
- const TOOL_SCHEMA = {
128
- type: 'object',
129
- properties: {
130
- designFile: { type: 'string', description: 'Absolute or relative Markdown design file path. Defaults to ../sql/SQL 设计说明.md when omitted.' },
131
- tableName: { type: 'string', description: 'Target main table name from the design file.' },
132
- style: { type: 'string', enum: Object.keys(STYLE_CATALOG), description: 'Style id from assets/style-catalog.json.' },
126
+ const TOOL_SCHEMA = {
127
+ type: 'object',
128
+ properties: {
129
+ featureTitle: { type: 'string', description: 'Feature title from the API document or requirement document.' },
130
+ tableName: { type: 'string', description: 'Target main table name from the structured feature metadata.' },
131
+ tableComment: { type: 'string', description: 'Main table comment or feature label for menu/title generation.' },
132
+ apiPath: { type: 'string', description: 'Backend API base path from the API document, for example iwmEmpOutsourcePerson.' },
133
+ style: { type: 'string', enum: Object.keys(STYLE_CATALOG), description: 'Style id from assets/style-catalog.json.' },
134
+ fields: {
135
+ type: 'array',
136
+ description: 'Structured main-table field metadata parsed from the API document and requirement document.',
137
+ items: {
138
+ type: 'object',
139
+ properties: {
140
+ fieldName: { type: 'string', description: 'Backend field name.' },
141
+ label: { type: 'string', description: 'Display label.' },
142
+ type: { type: 'string', description: 'Normalized or source field type, for example String, Long, Integer, LocalDate, LocalDateTime, DECIMAL, VARCHAR.' },
143
+ length: { type: ['string', 'number'], description: 'Field length or precision string, for example 30 or 10,2.' },
144
+ scale: { type: ['string', 'number'], description: 'Optional decimal scale.' },
145
+ required: { type: ['boolean', 'string'], description: 'Whether the field is required.' },
146
+ readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
147
+ show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
148
+ dictType: { type: 'string', description: 'Dictionary type code from the API document.' },
149
+ defaultValue: { type: ['string', 'number', 'boolean'], description: 'Optional default value.' },
150
+ description: { type: 'string', description: 'Field description or notes.' },
151
+ sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
152
+ primary: { type: ['boolean', 'string'], description: 'Whether the field is the primary key.' },
153
+ },
154
+ required: ['fieldName', 'label', 'type'],
155
+ additionalProperties: false,
156
+ },
157
+ },
133
158
  children: {
134
159
  type: 'array',
135
- description: 'Optional direct child-table relations for master_child_jump. When provided, MCP renders all listed direct child tables on the same main form.',
160
+ description: 'Optional direct child-table structures for master_child_jump. When provided, MCP renders all listed direct child tables on the same main form.',
136
161
  items: {
137
162
  type: 'object',
138
163
  properties: {
139
164
  childTableName: { type: 'string', description: 'Child table name.' },
165
+ childTableComment: { type: 'string', description: 'Child table comment or display label.' },
140
166
  mainField: { type: 'string', description: 'Main table relation field.' },
141
167
  childField: { type: 'string', description: 'Child table relation field.' },
142
- payloadField: { type: 'string', description: 'Optional backend payload list field name from the API document, for example certificateList.' },
168
+ payloadField: { type: 'string', description: 'Backend payload list field name from the API document, for example certificateList.' },
143
169
  relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
170
+ fields: {
171
+ type: 'array',
172
+ description: 'Structured child-table field metadata.',
173
+ items: {
174
+ type: 'object',
175
+ properties: {
176
+ fieldName: { type: 'string', description: 'Backend field name.' },
177
+ label: { type: 'string', description: 'Display label.' },
178
+ type: { type: 'string', description: 'Normalized or source field type.' },
179
+ length: { type: ['string', 'number'], description: 'Field length or precision string.' },
180
+ scale: { type: ['string', 'number'], description: 'Optional decimal scale.' },
181
+ required: { type: ['boolean', 'string'], description: 'Whether the field is required.' },
182
+ readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
183
+ show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
184
+ dictType: { type: 'string', description: 'Dictionary type code from the API document.' },
185
+ defaultValue: { type: ['string', 'number', 'boolean'], description: 'Optional default value.' },
186
+ description: { type: 'string', description: 'Field description or notes.' },
187
+ sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
188
+ primary: { type: ['boolean', 'string'], description: 'Whether the field is the primary key.' },
189
+ },
190
+ required: ['fieldName', 'label', 'type'],
191
+ additionalProperties: false,
192
+ },
193
+ },
144
194
  },
145
- required: ['childTableName', 'mainField', 'childField'],
195
+ required: ['childTableName', 'mainField', 'childField', 'payloadField', 'fields'],
146
196
  additionalProperties: false,
147
197
  },
148
198
  },
149
- childTableName: { type: 'string', description: 'Child table name. Required when style=master_child_jump.' },
150
- mainField: { type: 'string', description: 'Main table relation field. Required when style=master_child_jump.' },
151
- childField: { type: 'string', description: 'Child table relation field. Required when style=master_child_jump.' },
152
- childPayloadField: { type: 'string', description: 'Optional backend payload list field name for the legacy single-child mode, for example certificateList.' },
153
- relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
154
- frontendPath: { type: 'string', description: 'Absolute frontend output root path.' },
155
- moduleName: { type: 'string', description: 'Relative frontend module path, for example admin/test.' },
156
- writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
157
- overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing files. If false, existing files are skipped.' },
158
- },
159
- required: ['tableName', 'style', 'frontendPath'],
160
- additionalProperties: false,
161
- };
199
+ frontendPath: { type: 'string', description: 'Absolute frontend output root path.' },
200
+ moduleName: { type: 'string', description: 'Relative frontend module path, for example admin/test.' },
201
+ writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
202
+ overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing files. If false, existing files are skipped.' },
203
+ },
204
+ required: ['tableName', 'style', 'frontendPath', 'fields'],
205
+ additionalProperties: false,
206
+ };
162
207
 
163
208
  function loadStyleCatalog() {
164
209
  return JSON.parse(fs.readFileSync(STYLE_CATALOG_PATH, 'utf8'));
@@ -613,244 +658,124 @@ function splitLength(value) {
613
658
  return { length: parts[0] || '', scale: parts[1] || '' };
614
659
  }
615
660
 
616
- function normalizeDefaultValue(value) {
617
- const normalized = String(value || '').trim();
618
- if (!normalized || normalized === '-' || normalized === '/') {
619
- return '';
620
- }
621
- return normalized;
622
- }
623
-
624
- function parseRequiredFlag(value) {
625
- const normalized = String(value || '').trim().toLowerCase();
626
- return ['y', 'yes', '1', 'true', 'required'].includes(normalized);
627
- }
628
-
629
- function parseMarkdownRow(line) {
630
- const trimmed = line.trim();
631
- if (!trimmed.startsWith('|')) return [];
632
- return trimmed
633
- .slice(1, trimmed.endsWith('|') ? -1 : undefined)
634
- .split('|')
635
- .map((cell) => cell.trim());
636
- }
637
-
638
- function isMarkdownSeparatorRow(cells) {
639
- return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
640
- }
641
-
642
- function findHeaderIndex(headers, patterns) {
643
- return headers.findIndex((header) => patterns.some((pattern) => pattern.test(header)));
644
- }
645
-
646
- function findPrimaryKeyFromText(text, fields) {
647
- const quotedMatch = text.match(/PRIMARY\s+KEY\s*\(\s*`?([a-zA-Z0-9_]+)`?\s*\)/i);
648
- if (quotedMatch) {
649
- return quotedMatch[1];
650
- }
651
-
652
- const commentPk = fields.find((field) => /主键|primary\s+key|\bpk\b/i.test(field.comment));
653
- if (commentPk) {
654
- return commentPk.fieldName;
655
- }
656
-
657
- const idField = fields.find((field) => field.fieldName === 'id');
658
- if (idField) {
659
- return idField.fieldName;
660
- }
661
-
662
- return fields[0] ? fields[0].fieldName : null;
663
- }
664
-
665
- function parseMarkdownTableSection(tableName, tableComment, sectionLines) {
666
- const firstTableLineIndex = sectionLines.findIndex((line) => line.trim().startsWith('|'));
667
- if (firstTableLineIndex < 0) {
668
- throw new Error('Could not find markdown field table for ' + tableName);
669
- }
670
-
671
- const tableLines = [];
672
- for (let index = firstTableLineIndex; index < sectionLines.length; index += 1) {
673
- const line = sectionLines[index].trim();
674
- if (!line.startsWith('|')) break;
675
- tableLines.push(line);
676
- }
677
-
678
- const rows = tableLines.map(parseMarkdownRow).filter((cells) => cells.length > 0);
679
- if (rows.length < 3) {
680
- throw new Error('Markdown field table is incomplete for ' + tableName);
681
- }
682
-
683
- const headers = rows[0];
684
- const dataRows = rows.slice(1).filter((cells) => !isMarkdownSeparatorRow(cells));
685
- const fieldNameIndex = findHeaderIndex(headers, [/field/i, /\u5B57\u6BB5/i, /\u5B57\u6BB5\u540D/i, /\u5217\u540D/i]);
686
- const sqlTypeIndex = findHeaderIndex(headers, [/type/i, /\u7C7B\u578B/i, /\u5B57\u6BB5\u7C7B\u578B/i, /\u6570\u636E\u7C7B\u578B/i]);
687
- const lengthIndex = findHeaderIndex(headers, [/length/i, /\u957F\u5EA6/i, /\u5B57\u6BB5\u957F\u5EA6/i]);
688
- const requiredIndex = findHeaderIndex(headers, [/required/i, /\u5FC5\u586B/i, /\u662F\u5426\u5FC5\u586B/i, /\u4E0D\u80FD\u4E3A\u7A7A/i]);
689
- const defaultIndex = findHeaderIndex(headers, [/default/i, /\u9ED8\u8BA4/i, /\u9ED8\u8BA4\u503C/i]);
690
- const commentIndex = findHeaderIndex(headers, [/comment/i, /\u5907\u6CE8/i, /\u8BF4\u660E/i, /\u5B57\u6BB5\u8BF4\u660E/i]);
691
-
692
- const dictTypeIndex = findHeaderIndex(headers, [/dict/i, /dict_?type/i, /\u5B57\u5178/i, /\u5B57\u5178\u7C7B\u578B/i]);
693
-
694
- if (fieldNameIndex < 0 || sqlTypeIndex < 0) {
695
- throw new Error('Markdown field table headers are missing required columns for ' + tableName);
696
- }
697
-
698
- const fields = [];
699
- for (const row of dataRows) {
700
- const fieldName = String(row[fieldNameIndex] || '').trim();
701
- if (!fieldName || fieldName === '-') continue;
702
-
703
- const sqlType = String(row[sqlTypeIndex] || '').trim().toUpperCase();
704
- if (!sqlType) continue;
705
-
706
- const lengthRaw = lengthIndex >= 0 ? row[lengthIndex] : '';
707
- const comment = String(commentIndex >= 0 ? row[commentIndex] : '').trim() || fieldName;
708
- const explicitDictType = dictTypeIndex >= 0 ? row[dictTypeIndex] : '';
709
- const { length, scale } = splitLength(lengthRaw);
710
-
711
- fields.push({
712
- fieldName,
713
- attrName: toCamelCase(fieldName),
714
- sqlType,
715
- length,
716
- scale,
717
- comment,
718
- dictType: resolveDictType(comment, explicitDictType),
719
- notNull: requiredIndex >= 0 ? parseRequiredFlag(row[requiredIndex]) : false,
720
- defaultValue: defaultIndex >= 0 ? normalizeDefaultValue(row[defaultIndex]) : '',
721
- });
722
- }
723
-
724
- if (!fields.length) {
725
- throw new Error('No fields were parsed from markdown table ' + tableName);
726
- }
727
-
728
- const sectionText = sectionLines.join('\n');
729
- const pkName = findPrimaryKeyFromText(sectionText, fields);
730
- const pkField = fields.find((field) => field.fieldName === pkName) || fields[0];
731
- return { pkField, fields, tableComment };
732
- }
733
-
734
- function parseMarkdownDesignTables(markdownText) {
735
- const lines = stripBom(markdownText).replace(/\r\n?/g, '\n').split('\n');
736
- const tables = new Map();
737
-
738
- for (let index = 0; index < lines.length; index += 1) {
739
- const heading = lines[index].trim();
740
- const headingMatch = heading.match(/^###\s+\S+\s+([a-zA-Z0-9_]+)\s*[\(\uFF08]([^)\n\uFF09]+)[\)\uFF09]/);
741
- if (!headingMatch) continue;
742
-
743
- const tableName = headingMatch[1];
744
- const tableComment = headingMatch[2].trim();
745
- const sectionLines = [];
746
- let nextIndex = index + 1;
747
- while (nextIndex < lines.length && !/^(##|###)\s+/.test(lines[nextIndex])) {
748
- sectionLines.push(lines[nextIndex]);
749
- nextIndex += 1;
750
- }
751
-
752
- tables.set(tableName, parseMarkdownTableSection(tableName, tableComment, sectionLines));
753
- index = nextIndex - 1;
754
- }
755
-
756
- return tables;
757
- }
758
-
759
- function extractIdentifiersFromLine(line) {
760
- return [...String(line || '').matchAll(/`?([a-zA-Z][a-zA-Z0-9_]*)`?/g)].map((match) => match[1]);
761
- }
762
-
763
- function stripMarkdownSyntax(text) {
764
- return String(text || '')
765
- .replace(/\*\*/g, '')
766
- .replace(/`/g, '')
767
- .replace(/\r\n?/g, '\n');
768
- }
769
-
770
- function parseMarkdownRelations(markdownText) {
771
- void markdownText;
772
- return [];
661
+ function normalizeDefaultValue(value) {
662
+ const normalized = String(value || '').trim();
663
+ if (!normalized || normalized === '-' || normalized === '/') {
664
+ return '';
665
+ }
666
+ return normalized;
773
667
  }
774
668
 
775
- function parseDesignFeatureTitle(markdownText) {
776
- const lines = stripBom(String(markdownText || '')).replace(/\r\n?/g, '\n').split('\n');
777
- const titleLine = lines.find((line) => /^#\s+/.test(line.trim()));
778
- if (!titleLine) {
779
- return '';
669
+ function parseBooleanLike(value, defaultValue = false) {
670
+ if (value === undefined || value === null || value === '') {
671
+ return defaultValue;
780
672
  }
781
- const rawTitle = titleLine.trim().replace(/^#\s+/, '').trim();
782
- return rawTitle.replace(/\s*-\s*SQL\s*设计说明\s*$/i, '').trim();
673
+ if (typeof value === 'boolean') {
674
+ return value;
675
+ }
676
+ const normalized = String(value).trim().toLowerCase();
677
+ if (['true', '1', 'yes', 'y', 'required', '是', '显示'].includes(normalized)) {
678
+ return true;
679
+ }
680
+ if (['false', '0', 'no', 'n', '否', '不显示'].includes(normalized)) {
681
+ return false;
682
+ }
683
+ return defaultValue;
783
684
  }
784
685
 
785
- function parseMarkdownDesignFile(markdownText) {
686
+ function normalizeStructuredSourceKind(value) {
687
+ const normalized = String(value || '').trim().toLowerCase();
688
+ if (['entity', 'display', 'virtual', 'common'].includes(normalized)) {
689
+ return normalized;
690
+ }
691
+ return 'entity';
692
+ }
693
+
694
+ function normalizeApiPath(value) {
695
+ return String(value || '')
696
+ .trim()
697
+ .replace(/^\/+/, '')
698
+ .replace(/\/+$/, '');
699
+ }
700
+
701
+ function normalizeStructuredSqlType(value) {
702
+ const normalized = String(value || '').trim();
703
+ const upper = normalized.replace(/\s+/g, '').toUpperCase();
704
+
705
+ if (!upper) return 'VARCHAR';
706
+ if (['STRING', 'TEXT', 'VARCHAR', 'CHAR'].includes(upper) || ['文本', '字符', '字符串'].includes(normalized)) return 'VARCHAR';
707
+ if (['LONG', 'BIGINT', 'LONGINTEGER'].includes(upper) || ['长整数', '长整型'].includes(normalized)) return 'BIGINT';
708
+ if (['INT', 'INTEGER', 'SHORT', 'SMALLINT'].includes(upper) || ['整数', '整型'].includes(normalized)) return 'INT';
709
+ if (['DECIMAL', 'NUMERIC', 'BIGDECIMAL', 'DOUBLE', 'FLOAT'].includes(upper) || ['小数', '金额'].includes(normalized)) return 'DECIMAL';
710
+ if (['LOCALDATE', 'DATE'].includes(upper) || normalized === '日期') return 'DATE';
711
+ if (['LOCALDATETIME', 'DATETIME', 'TIMESTAMP'].includes(upper) || ['日期时间', '时间'].includes(normalized)) return 'DATETIME';
712
+ if (['TEXTAREA', 'LONGTEXT', 'TEXT'].includes(upper) || ['长文本', '多行文本'].includes(normalized)) return 'TEXT';
713
+ return upper;
714
+ }
715
+
716
+ function normalizeStructuredLengthAndScale(lengthValue, scaleValue) {
717
+ const parsed = splitLength(lengthValue);
718
+ const normalizedScale = scaleValue === undefined || scaleValue === null || scaleValue === '' ? parsed.scale : String(scaleValue).trim();
786
719
  return {
787
- featureTitle: parseDesignFeatureTitle(markdownText),
788
- tables: parseMarkdownDesignTables(markdownText),
789
- relations: [],
720
+ length: parsed.length,
721
+ scale: normalizedScale || '',
790
722
  };
791
723
  }
724
+
725
+ function normalizeStructuredField(inputField, index, contextLabel) {
726
+ if (!inputField || typeof inputField !== 'object') {
727
+ throw new Error(contextLabel + '[' + index + '] must be an object');
728
+ }
729
+
730
+ const fieldName = String(inputField.fieldName || '').trim();
731
+ const label = String(inputField.label || inputField.description || fieldName).trim();
732
+ const type = normalizeStructuredSqlType(inputField.type);
733
+
734
+ if (!fieldName) {
735
+ throw new Error(contextLabel + '[' + index + '] is missing required field: fieldName');
736
+ }
737
+ if (!label) {
738
+ throw new Error(contextLabel + '[' + index + '] is missing required field: label');
739
+ }
740
+ if (!String(inputField.type || '').trim()) {
741
+ throw new Error(contextLabel + '[' + index + '] is missing required field: type');
742
+ }
743
+
744
+ const { length, scale } = normalizeStructuredLengthAndScale(inputField.length, inputField.scale);
745
+
746
+ return {
747
+ fieldName,
748
+ attrName: toCamelCase(fieldName),
749
+ sqlType: type,
750
+ length,
751
+ scale,
752
+ comment: label,
753
+ label,
754
+ description: String(inputField.description || '').trim(),
755
+ dictType: normalizeDictType(inputField.dictType),
756
+ notNull: parseBooleanLike(inputField.required, false),
757
+ defaultValue: normalizeDefaultValue(inputField.defaultValue),
758
+ readonly: parseBooleanLike(inputField.readonly, false),
759
+ show: parseBooleanLike(inputField.show, true),
760
+ sourceKind: normalizeStructuredSourceKind(inputField.sourceKind),
761
+ primary: parseBooleanLike(inputField.primary, fieldName === 'id'),
762
+ };
763
+ }
764
+
765
+ function normalizeStructuredFieldArray(inputFields, contextLabel) {
766
+ if (!Array.isArray(inputFields) || !inputFields.length) {
767
+ throw new Error(contextLabel + ' must be a non-empty array');
768
+ }
769
+ return inputFields.map((field, index) => normalizeStructuredField(field, index, contextLabel));
770
+ }
771
+
772
+ function findPrimaryKeyFromStructuredFields(fields) {
773
+ return fields.find((field) => field.primary) || fields.find((field) => field.fieldName === 'id') || fields[0];
774
+ }
792
775
 
793
- function parseTableFromMarkdownDesign(designDoc, tableName) {
794
- const parsed = designDoc.tables.get(tableName);
795
- if (!parsed) {
796
- throw new Error('Could not find table definition for ' + tableName + ' in Markdown design file');
797
- }
798
- return parsed;
799
- }
800
-
801
- function buildQuestionMarkSegmentRegex(segment) {
802
- return new RegExp('^' + escapeForRegex(segment).replace(/\\\?/g, '.') + '$', 'i');
803
- }
804
-
805
- function tryResolveGarbledPath(resolvedPath) {
806
- if (!resolvedPath.includes('?')) {
807
- return null;
808
- }
809
-
810
- const parsed = path.parse(resolvedPath);
811
- const segments = resolvedPath.slice(parsed.root.length).split(path.sep).filter(Boolean);
812
- let currentPath = parsed.root;
813
-
814
- for (const segment of segments) {
815
- const exactPath = path.join(currentPath, segment);
816
- if (fs.existsSync(exactPath)) {
817
- currentPath = exactPath;
818
- continue;
819
- }
820
-
821
- if (!segment.includes('?') || !fs.existsSync(currentPath) || !fs.statSync(currentPath).isDirectory()) {
822
- return null;
823
- }
824
-
825
- const matcher = buildQuestionMarkSegmentRegex(segment);
826
- const matches = fs.readdirSync(currentPath).filter((name) => matcher.test(name));
827
- if (matches.length !== 1) {
828
- throw new Error(
829
- 'Source file path could not be resolved uniquely from garbled input: ' +
830
- resolvedPath +
831
- '. Matched entries: ' +
832
- (matches.length ? matches.join(', ') : 'none')
833
- );
834
- }
835
-
836
- currentPath = path.join(currentPath, matches[0]);
837
- }
838
-
839
- return fs.existsSync(currentPath) ? currentPath : null;
840
- }
841
-
842
- function resolveSourcePath(inputPath) {
843
- const resolvedPath = path.resolve(inputPath);
844
- if (fs.existsSync(resolvedPath)) {
845
- return resolvedPath;
846
- }
847
-
848
- const recoveredPath = tryResolveGarbledPath(resolvedPath);
849
- if (recoveredPath) {
850
- return recoveredPath;
851
- }
852
-
853
- throw new Error('Source file does not exist: ' + resolvedPath);
776
+ function parseRequiredFlag(value) {
777
+ const normalized = String(value || '').trim().toLowerCase();
778
+ return ['y', 'yes', '1', 'true', 'required'].includes(normalized);
854
779
  }
855
780
 
856
781
  function formatRelationCandidate(relation) {
@@ -866,107 +791,31 @@ function formatRelationCandidate(relation) {
866
791
  return candidate;
867
792
  }
868
793
 
869
- function getRelationCandidates(designDoc, tableName) {
870
- return designDoc.relations
871
- .filter((relation) => relation.mainTableName === tableName)
872
- .map(formatRelationCandidate);
873
- }
874
-
875
- function buildRetryArguments(safeArgs, sourceFile, childTableName) {
876
- const retryArguments = {
877
- tableName: safeArgs.tableName,
878
- style: safeArgs.style,
879
- frontendPath: safeArgs.frontendPath,
880
- moduleName: safeArgs.moduleName,
881
- writeToDisk: safeArgs.writeToDisk,
882
- overwrite: safeArgs.overwrite,
883
- };
884
-
885
- if (sourceFile) {
886
- retryArguments.designFile = sourceFile;
887
- }
888
-
794
+ function buildRetryArguments(safeArgs) {
795
+ const retryArguments = {
796
+ ...(safeArgs.featureTitle ? { featureTitle: safeArgs.featureTitle } : {}),
797
+ tableName: safeArgs.tableName,
798
+ ...(safeArgs.tableComment ? { tableComment: safeArgs.tableComment } : {}),
799
+ style: safeArgs.style,
800
+ frontendPath: safeArgs.frontendPath,
801
+ moduleName: safeArgs.moduleName,
802
+ writeToDisk: safeArgs.writeToDisk,
803
+ overwrite: safeArgs.overwrite,
804
+ fields: safeArgs.fields,
805
+ };
806
+
889
807
  if (safeArgs.children && safeArgs.children.length) {
890
- retryArguments.children = safeArgs.children.map((relation) => ({
891
- childTableName: relation.childTableName,
892
- mainField: relation.mainField,
893
- childField: relation.childField,
894
- ...(relation.payloadField ? { payloadField: relation.payloadField } : {}),
895
- relationType: relation.relationType || '',
896
- }));
897
- }
898
-
899
- if (childTableName) {
900
- retryArguments.childTableName = childTableName;
901
- }
902
-
903
- if (safeArgs.mainField) {
904
- retryArguments.mainField = safeArgs.mainField;
905
- }
906
-
907
- if (safeArgs.childField) {
908
- retryArguments.childField = safeArgs.childField;
808
+ retryArguments.children = safeArgs.children;
909
809
  }
910
810
 
911
- if (safeArgs.childPayloadField) {
912
- retryArguments.childPayloadField = safeArgs.childPayloadField;
913
- }
914
-
915
- if (safeArgs.relationType) {
916
- retryArguments.relationType = safeArgs.relationType;
917
- }
918
-
919
- return retryArguments;
920
- }
921
-
922
- function buildRelationCorrectionEntry(safeArgs, sourceFile, candidates, selectedChildTableName) {
923
- return {
924
- field: 'childTableName',
925
- title: '请选择子表',
926
- tableName: safeArgs.tableName,
927
- style: safeArgs.style,
928
- designFile: sourceFile,
929
- description: candidates.length
930
- ? 'If the current child relation is not the one you want, pass childTableName to choose a candidate from the design file.'
931
- : 'No child relation was found. Check the child relation section in the design file.',
932
- currentValue: selectedChildTableName || '',
933
- options: candidates.map((item) => item.childTableName),
934
- candidates,
935
- example: candidates.length ? buildRetryArguments(safeArgs, sourceFile, candidates[0].childTableName) : buildRetryArguments(safeArgs, sourceFile, selectedChildTableName),
936
- retryArguments: candidates.map((item) => buildRetryArguments(safeArgs, sourceFile, item.childTableName)),
937
- };
938
- }
939
-
940
- function createRelationResolutionError(message, safeArgs, sourceFile, candidates, selectedChildTableName, status) {
941
- const error = new Error(message);
942
- error.details = {
943
- type: 'relation_resolution',
944
- status,
945
- tableName: safeArgs.tableName,
946
- designFile: sourceFile,
947
- message,
948
- candidates,
949
- correctionEntry: buildRelationCorrectionEntry(safeArgs, sourceFile, candidates, selectedChildTableName),
950
- };
951
- return error;
952
- }
953
-
954
- function loadSourceDocument(safeArgs) {
955
- const inputPath = safeArgs.designFile || DEFAULT_DESIGN_FILE;
956
- const sourcePath = resolveSourcePath(inputPath);
957
- const text = readUtf8File(sourcePath);
958
- return {
959
- path: sourcePath,
960
- text,
961
- designDoc: parseMarkdownDesignFile(text),
962
- };
963
- }
811
+ return retryArguments;
812
+ }
964
813
 
965
814
  function normalizeChildrenInput(inputChildren) {
966
- if (inputChildren === undefined || inputChildren === null) {
967
- return [];
968
- }
969
- if (!Array.isArray(inputChildren)) {
815
+ if (inputChildren === undefined || inputChildren === null) {
816
+ return [];
817
+ }
818
+ if (!Array.isArray(inputChildren)) {
970
819
  throw new Error('children must be an array');
971
820
  }
972
821
  return inputChildren.map((item, index) => {
@@ -974,79 +823,26 @@ function normalizeChildrenInput(inputChildren) {
974
823
  throw new Error('children[' + index + '] must be an object');
975
824
  }
976
825
  const childTableName = item.childTableName ? String(item.childTableName) : '';
826
+ const childTableComment = item.childTableComment ? String(item.childTableComment) : '';
977
827
  const mainField = item.mainField ? String(item.mainField) : '';
978
828
  const childField = item.childField ? String(item.childField) : '';
979
829
  const payloadField = item.payloadField ? String(item.payloadField) : '';
980
830
  const relationType = item.relationType ? String(item.relationType) : '';
981
- const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !({ childTableName, mainField, childField })[field]);
831
+ const fields = normalizeStructuredFieldArray(item.fields, 'children[' + index + '].fields');
832
+ const missingFields = ['childTableName', 'mainField', 'childField', 'payloadField'].filter(
833
+ (field) => !({ childTableName, mainField, childField, payloadField })[field]
834
+ );
982
835
  if (missingFields.length) {
983
836
  throw new Error('children[' + index + '] is missing required fields: ' + missingFields.join(', '));
984
837
  }
985
- return { childTableName, mainField, childField, payloadField, relationType };
838
+ return { childTableName, childTableComment, mainField, childField, payloadField, relationType, fields };
986
839
  });
987
840
  }
988
-
989
- function resolveMarkdownChildRelations(sourceDocument, safeArgs) {
990
- if (safeArgs.children && safeArgs.children.length) {
991
- return safeArgs.children.map((relation) => ({
992
- mainTableName: safeArgs.tableName,
993
- childTableName: relation.childTableName,
994
- mainField: relation.mainField,
995
- childField: relation.childField,
996
- payloadField: relation.payloadField || '',
997
- relationType: relation.relationType || '',
998
- }));
999
- }
1000
-
1001
- const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !safeArgs[field]);
1002
- if (missingFields.length) {
1003
- const message = 'master_child_jump requires either children[] or legacy relation fields. Missing: ' + missingFields.join(', ') + '.';
1004
- const error = new Error(message);
1005
- error.details = {
1006
- type: 'relation_input_required',
1007
- status: 'missing_required_relation_input',
1008
- tableName: safeArgs.tableName,
1009
- designFile: sourceDocument.path,
1010
- message,
1011
- requiredFields: ['children[] or childTableName/mainField/childField'],
1012
- correctionEntry: {
1013
- fields: ['children', 'childTableName', 'mainField', 'childField'],
1014
- example: {
1015
- ...buildRetryArguments(safeArgs, sourceDocument.path, safeArgs.childTableName || '<child_table_name>'),
1016
- children: [
1017
- {
1018
- childTableName: safeArgs.childTableName || '<child_table_name>',
1019
- mainField: safeArgs.mainField || '<main_field>',
1020
- childField: safeArgs.childField || '<child_field>',
1021
- ...(safeArgs.childPayloadField ? { payloadField: safeArgs.childPayloadField } : {}),
1022
- relationType: safeArgs.relationType || '1:N',
1023
- },
1024
- ],
1025
- mainField: safeArgs.mainField || '<main_field>',
1026
- childField: safeArgs.childField || '<child_field>',
1027
- ...(safeArgs.childPayloadField ? { childPayloadField: safeArgs.childPayloadField } : {}),
1028
- },
1029
- },
1030
- };
1031
- throw error;
1032
- }
1033
-
1034
- return [
1035
- {
1036
- mainTableName: safeArgs.tableName,
1037
- childTableName: safeArgs.childTableName,
1038
- mainField: safeArgs.mainField,
1039
- childField: safeArgs.childField,
1040
- payloadField: safeArgs.childPayloadField || '',
1041
- relationType: safeArgs.relationType || '',
1042
- },
1043
- ];
1044
- }
1045
-
1046
- function getStylePreset(styleId) {
1047
- const preset = STYLE_CATALOG[styleId];
1048
- if (!preset) throw new Error('Unsupported style: ' + styleId);
1049
- return preset;
841
+
842
+ function getStylePreset(styleId) {
843
+ const preset = STYLE_CATALOG[styleId];
844
+ if (!preset) throw new Error('Unsupported style: ' + styleId);
845
+ return preset;
1050
846
  }
1051
847
 
1052
848
  function resolveSharedTemplates(stylePreset) {
@@ -1066,70 +862,74 @@ function normalizeFields(parsed) {
1066
862
  return parsed.fields.map((field) => ({ ...field, formType: mapFieldType(field), isAudit: isAuditField(field.fieldName) }));
1067
863
  }
1068
864
 
1069
- function ensureFieldExists(fields, fieldName, tableName, role) {
1070
- const field = fields.find((item) => item.fieldName === fieldName);
1071
- if (!field) throw new Error(role + ' field "' + fieldName + '" was not found on table ' + tableName);
1072
- return field;
1073
- }
1074
-
1075
- function buildChildModels(sourceDocument, safeArgs, mainParsed) {
865
+ function ensureFieldExists(fields, fieldName, tableName, role) {
866
+ const field = fields.find((item) => item.fieldName === fieldName);
867
+ if (!field) throw new Error(role + ' field "' + fieldName + '" was not found on table ' + tableName);
868
+ return field;
869
+ }
870
+
871
+ function buildChildModels(safeArgs, mainFields, mainPk) {
1076
872
  if (safeArgs.style !== 'master_child_jump') return [];
1077
873
 
1078
- const relations = resolveMarkdownChildRelations(sourceDocument, safeArgs);
1079
- return relations.map((relation) => {
1080
- const childParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, relation.childTableName);
1081
- const childFields = normalizeFields(childParsed);
1082
- const mainRelationField = ensureFieldExists(mainParsed.fields, relation.mainField, safeArgs.tableName, 'Main relation');
1083
- const childRelationField = ensureFieldExists(childParsed.fields, relation.childField, relation.childTableName, 'Child relation');
874
+ if (!safeArgs.children.length) {
875
+ throw new Error('master_child_jump requires children[] with structured child table metadata');
876
+ }
877
+
878
+ return safeArgs.children.map((relation) => {
879
+ const childPk = findPrimaryKeyFromStructuredFields(relation.fields);
880
+ const childFields = normalizeFields({
881
+ fields: relation.fields,
882
+ });
883
+ const mainRelationField = ensureFieldExists(mainFields, relation.mainField, safeArgs.tableName, 'Main relation');
884
+ const childRelationField = ensureFieldExists(childFields, relation.childField, relation.childTableName, 'Child relation');
1084
885
  const childVisibleFields = childFields.filter(
1085
- (field) => field.fieldName !== childParsed.pkField.fieldName && !field.isAudit && field.fieldName !== relation.childField
886
+ (field) => field.fieldName !== childPk.fieldName && !field.isAudit && field.fieldName !== relation.childField && field.show !== false
1086
887
  );
1087
- const payloadField =
1088
- relation.payloadField || (relations.length === 1 && safeArgs.childPayloadField ? safeArgs.childPayloadField : '') || '';
1089
- const listName = payloadField || toCamelCase(relation.childTableName) + 'List';
888
+ const payloadField = relation.payloadField;
889
+ const listName = payloadField;
1090
890
 
1091
891
  return {
1092
892
  tableName: relation.childTableName,
1093
- tableComment: childParsed.tableComment,
893
+ tableComment: relation.childTableComment || relation.childTableName,
1094
894
  className: toPascalCase(relation.childTableName),
1095
895
  functionName: toCamelCase(relation.childTableName),
1096
896
  listName,
1097
897
  payloadField,
1098
- payloadFieldSource: payloadField ? 'arguments' : 'derived',
1099
- pk: childParsed.pkField,
898
+ payloadFieldSource: 'arguments',
899
+ pk: childPk,
1100
900
  fields: childFields,
1101
901
  visibleFields: childVisibleFields,
1102
- mainField: mainRelationField,
1103
- childField: childRelationField,
1104
- relationType: relation.relationType,
902
+ mainField: mainRelationField,
903
+ childField: childRelationField,
904
+ relationType: relation.relationType,
1105
905
  };
1106
906
  });
1107
- }
1108
-
1109
- function buildModel(safeArgs) {
1110
- const sourceDocument = loadSourceDocument(safeArgs);
1111
- const mainParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, safeArgs.tableName);
1112
- const fields = normalizeFields(mainParsed);
1113
- const visibleFields = fields.filter((field) => field.fieldName !== mainParsed.pkField.fieldName && !field.isAudit);
1114
- const gridFields = visibleFields.slice(0, 8);
1115
- const children = buildChildModels(sourceDocument, safeArgs, mainParsed);
1116
- const childDictTypes = children.flatMap((child) => child.visibleFields.map((field) => field.dictType).filter(Boolean));
1117
- const dictTypes = [...new Set([...visibleFields.map((field) => field.dictType).filter(Boolean), ...childDictTypes])];
1118
-
907
+ }
908
+
909
+ function buildModel(safeArgs) {
910
+ const pkField = findPrimaryKeyFromStructuredFields(safeArgs.fields);
911
+ const fields = normalizeFields({
912
+ fields: safeArgs.fields,
913
+ });
914
+ const visibleFields = fields.filter((field) => field.fieldName !== pkField.fieldName && !field.isAudit && field.show !== false);
915
+ const gridFields = visibleFields.slice(0, 8);
916
+ const children = buildChildModels(safeArgs, fields, pkField);
917
+ const childDictTypes = children.flatMap((child) => child.visibleFields.map((field) => field.dictType).filter(Boolean));
918
+ const dictTypes = [...new Set([...visibleFields.map((field) => field.dictType).filter(Boolean), ...childDictTypes])];
919
+
1119
920
  return {
1120
- sourceFile: sourceDocument.path,
1121
- designFile: sourceDocument.path,
1122
- featureTitle: sourceDocument.designDoc.featureTitle || mainParsed.tableComment,
921
+ featureTitle: safeArgs.featureTitle || safeArgs.tableComment || safeArgs.tableName,
1123
922
  tableName: safeArgs.tableName,
1124
- tableComment: mainParsed.tableComment,
1125
- className: toPascalCase(safeArgs.tableName),
1126
- functionName: toCamelCase(safeArgs.tableName),
1127
- moduleName: normalizeModuleName(safeArgs.moduleName),
1128
- pk: mainParsed.pkField,
1129
- fields,
1130
- visibleFields,
1131
- gridFields,
1132
- dictTypes,
923
+ tableComment: safeArgs.tableComment || safeArgs.featureTitle || safeArgs.tableName,
924
+ apiPath: safeArgs.apiPath || toCamelCase(safeArgs.tableName),
925
+ className: toPascalCase(safeArgs.tableName),
926
+ functionName: toCamelCase(safeArgs.tableName),
927
+ moduleName: normalizeModuleName(safeArgs.moduleName),
928
+ pk: pkField,
929
+ fields,
930
+ visibleFields,
931
+ gridFields,
932
+ dictTypes,
1133
933
  frontendPath: path.resolve(safeArgs.frontendPath),
1134
934
  style: safeArgs.style,
1135
935
  children,
@@ -1264,11 +1064,11 @@ function renderDefaultLine(field) {
1264
1064
  return ` ${field.attrName}: '',`;
1265
1065
  }
1266
1066
 
1267
- function renderFormDefaults(model) {
1268
- const lines = [` ${model.pk.attrName}: '',`];
1269
- for (const field of model.visibleFields) lines.push(renderDefaultLine(field));
1270
- return lines.join('\n');
1271
- }
1067
+ function renderFormDefaults(model) {
1068
+ const lines = [` ${model.pk.attrName}: '',`];
1069
+ for (const field of model.fields.filter((item) => item.fieldName !== model.pk.fieldName && !item.isAudit)) lines.push(renderDefaultLine(field));
1070
+ return lines.join('\n');
1071
+ }
1272
1072
 
1273
1073
  function renderChildTempDefaults(childModel) {
1274
1074
  if (!childModel) return '';
@@ -1571,10 +1371,10 @@ function renderFormRulesV2(visibleFields) {
1571
1371
  return lines.join('\n');
1572
1372
  }
1573
1373
 
1574
- function buildReplacements(model, sharedSupport) {
1575
- const menuBaseId = Date.now();
1576
- const apiModulePath = `${model.moduleName}/${model.functionName}`;
1577
- const routePath = `${model.moduleName}/${model.functionName}`;
1374
+ function buildReplacements(model, sharedSupport) {
1375
+ const menuBaseId = Date.now();
1376
+ const apiModulePath = model.apiPath || `${model.moduleName}/${model.functionName}`;
1377
+ const routePath = `${model.moduleName}/${model.functionName}`;
1578
1378
  const permissionPrefix = `${model.moduleName}/${model.functionName}`.replace(/\//g, '_');
1579
1379
  const dictRegistryRefs = sharedSupport.dictRegistry.keyByValue;
1580
1380
  const i18nNamespace = buildI18nNamespace(model);
@@ -1649,30 +1449,29 @@ function renderFiles(model, stylePreset, sharedSupport, localeZhSupport) {
1649
1449
  return files;
1650
1450
  }
1651
1451
 
1652
- function ensureArguments(input) {
1653
- if (!input || typeof input !== 'object') throw new Error('Arguments must be an object');
1654
- for (const key of TOOL_SCHEMA.required) {
1655
- if (!input[key]) throw new Error(key + ' is required');
1656
- }
1657
-
1658
- const style = String(input.style);
1659
- getStylePreset(style);
1660
-
1661
- return {
1662
- designFile: input.designFile ? String(input.designFile) : null,
1663
- tableName: String(input.tableName),
1664
- style,
1452
+ function ensureArguments(input) {
1453
+ if (!input || typeof input !== 'object') throw new Error('Arguments must be an object');
1454
+ for (const key of TOOL_SCHEMA.required) {
1455
+ if (input[key] === undefined || input[key] === null || input[key] === '') throw new Error(key + ' is required');
1456
+ }
1457
+
1458
+ const style = String(input.style);
1459
+ getStylePreset(style);
1460
+ const fields = normalizeStructuredFieldArray(input.fields, 'fields');
1461
+
1462
+ return {
1463
+ featureTitle: input.featureTitle ? String(input.featureTitle) : '',
1464
+ tableName: String(input.tableName),
1465
+ tableComment: input.tableComment ? String(input.tableComment) : '',
1466
+ apiPath: normalizeApiPath(input.apiPath),
1467
+ style,
1468
+ fields,
1665
1469
  children: normalizeChildrenInput(input.children),
1666
- childTableName: input.childTableName ? String(input.childTableName) : null,
1667
- mainField: input.mainField ? String(input.mainField) : null,
1668
- childField: input.childField ? String(input.childField) : null,
1669
- childPayloadField: input.childPayloadField ? String(input.childPayloadField) : null,
1670
- relationType: input.relationType ? String(input.relationType) : '',
1671
- frontendPath: String(input.frontendPath),
1672
- moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
1673
- writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
1674
- overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
1675
- };
1470
+ frontendPath: String(input.frontendPath),
1471
+ moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
1472
+ writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
1473
+ overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
1474
+ };
1676
1475
  }
1677
1476
 
1678
1477
  function maybeWriteFiles(files, writeToDisk, overwrite) {
@@ -1696,7 +1495,7 @@ function maybeWriteFiles(files, writeToDisk, overwrite) {
1696
1495
  }
1697
1496
  }
1698
1497
 
1699
- function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, note) {
1498
+ function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, note) {
1700
1499
  const relations = model.children.map((childModel) => ({
1701
1500
  childTableName: childModel.tableName,
1702
1501
  childTableComment: childModel.tableComment,
@@ -1709,39 +1508,37 @@ function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, not
1709
1508
  }));
1710
1509
  const selectedList = relations.map((relation) => formatRelationCandidate(relation));
1711
1510
 
1712
- return {
1713
- mode: 'local-template',
1714
- style: safeArgs.style,
1715
- styleLabel: stylePreset.label,
1716
- runtimeSupported: hasRuntimeSupport(stylePreset),
1717
- sourceFile: model.sourceFile,
1718
- designFile: model.designFile,
1719
- tableName: model.tableName,
1720
- tableComment: model.tableComment,
1721
- moduleName: model.moduleName,
1722
- writeToDisk: safeArgs.writeToDisk,
1511
+ return {
1512
+ mode: 'local-template',
1513
+ style: safeArgs.style,
1514
+ styleLabel: stylePreset.label,
1515
+ runtimeSupported: hasRuntimeSupport(stylePreset),
1516
+ tableName: model.tableName,
1517
+ tableComment: model.tableComment,
1518
+ apiPath: model.apiPath,
1519
+ moduleName: model.moduleName,
1520
+ writeToDisk: safeArgs.writeToDisk,
1723
1521
  selectedTemplates: sharedTemplates,
1724
- files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
1725
- relation: relations.length === 1 ? relations[0] : null,
1726
- relations,
1727
- relationResolution: model.children.length
1728
- ? {
1729
- status: 'provided',
1730
- tableName: safeArgs.tableName,
1731
- designFile: model.designFile,
1732
- source: 'arguments',
1733
- message:
1734
- model.children.length > 1
1735
- ? 'Direct child relations were provided by the caller. MCP skipped design-doc relation inference and generated a single main form with multiple child tables.'
1736
- : 'The relation was provided by the caller. MCP skipped design-doc relation inference and generated files directly.',
1737
- selected: selectedList.length === 1 ? selectedList[0] : null,
1738
- selectedList,
1739
- correctionEntry: {
1740
- fields: ['children', 'childTableName', 'mainField', 'childField'],
1741
- example: buildRetryArguments(safeArgs, model.designFile, model.children[0].tableName),
1742
- },
1743
- }
1744
- : null,
1522
+ files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
1523
+ relation: relations.length === 1 ? relations[0] : null,
1524
+ relations,
1525
+ relationResolution: model.children.length
1526
+ ? {
1527
+ status: 'structured_input',
1528
+ tableName: safeArgs.tableName,
1529
+ source: 'arguments',
1530
+ message:
1531
+ model.children.length > 1
1532
+ ? 'Direct child relations were provided by the caller as structured API-first metadata. MCP generated a single main form with multiple child tables.'
1533
+ : 'The relation was provided by the caller as structured API-first metadata. MCP generated files directly.',
1534
+ selected: selectedList.length === 1 ? selectedList[0] : null,
1535
+ selectedList,
1536
+ correctionEntry: {
1537
+ fields: ['children'],
1538
+ example: buildRetryArguments(safeArgs),
1539
+ },
1540
+ }
1541
+ : null,
1745
1542
  summary: {
1746
1543
  totalFields: model.fields.length,
1747
1544
  visibleFields: model.visibleFields.length,
@@ -1808,7 +1605,18 @@ async function onMessage(message) {
1808
1605
  }
1809
1606
 
1810
1607
  if (method === 'tools/list') {
1811
- 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 }] }));
1608
+ writeMessage(
1609
+ successResponse(id, {
1610
+ tools: [
1611
+ {
1612
+ name: TOOL_NAME,
1613
+ description:
1614
+ 'Generate Worsoft frontend files and menu SQL from API-first structured feature metadata. In master_child_jump mode, the caller must provide children[] with explicit payloadField and child field metadata.',
1615
+ inputSchema: TOOL_SCHEMA,
1616
+ },
1617
+ ],
1618
+ })
1619
+ );
1812
1620
  return;
1813
1621
  }
1814
1622