@type-crafter/mcp 0.6.0 → 1.1.0
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 +203 -221
- package/dist/docs/RULES_COMPOSITION.md +343 -0
- package/dist/docs/RULES_NULLABLE.md +293 -0
- package/dist/docs/RULES_PATTERNS.md +516 -0
- package/dist/docs/RULES_REFERENCES.md +302 -0
- package/dist/docs/RULES_STRUCTURE.md +248 -0
- package/dist/docs/RULES_TYPES.md +291 -0
- package/dist/docs/WRITING_GUIDE.md +179 -0
- package/dist/index.js +321 -345
- package/package.json +3 -5
- package/src/GUIDE.md +0 -459
- package/src/SPEC_RULES.md +0 -1755
package/dist/index.js
CHANGED
|
@@ -3,58 +3,81 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
6
8
|
import { parse as parseYaml } from 'yaml';
|
|
7
|
-
import {
|
|
8
|
-
import { promisify } from 'util';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
9
10
|
import { z } from 'zod';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
// ES module dirname workaround
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
// Session management
|
|
15
|
+
const sessions = new Map();
|
|
16
|
+
const SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
17
|
+
function createSession() {
|
|
18
|
+
const sessionId = randomUUID();
|
|
19
|
+
sessions.set(sessionId, { createdAt: new Date(), acknowledged: true });
|
|
20
|
+
return sessionId;
|
|
21
|
+
}
|
|
22
|
+
function isValidSession(sessionId) {
|
|
23
|
+
if (!sessionId)
|
|
24
|
+
return false;
|
|
25
|
+
const session = sessions.get(sessionId);
|
|
26
|
+
if (!session)
|
|
27
|
+
return false;
|
|
28
|
+
// Check if session is expired
|
|
29
|
+
const now = new Date();
|
|
30
|
+
if (now.getTime() - session.createdAt.getTime() > SESSION_TTL_MS) {
|
|
31
|
+
sessions.delete(sessionId);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
// Clean up expired sessions periodically
|
|
37
|
+
setInterval(() => {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
for (const [id, session] of sessions.entries()) {
|
|
40
|
+
if (now.getTime() - session.createdAt.getTime() > SESSION_TTL_MS) {
|
|
41
|
+
sessions.delete(id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, 5 * 60 * 1000); // Every 5 minutes
|
|
45
|
+
// Zod schemas for tools
|
|
46
|
+
const getWritingGuideSchema = z.object({});
|
|
47
|
+
const getRulesSectionSchema = z.object({
|
|
48
|
+
sessionId: z
|
|
49
|
+
.string()
|
|
24
50
|
.optional()
|
|
25
|
-
.describe('
|
|
51
|
+
.describe('Session ID from get-writing-guide. Recommended but not required.'),
|
|
52
|
+
section: z
|
|
53
|
+
.enum(['structure', 'types', 'nullable', 'references', 'composition', 'patterns'])
|
|
54
|
+
.describe('The section to retrieve detailed rules for'),
|
|
26
55
|
});
|
|
27
56
|
const validateSpecSchema = z.object({
|
|
57
|
+
sessionId: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Session ID from get-writing-guide. Recommended but not required.'),
|
|
28
61
|
specFilePath: z.string().describe('Path to the YAML specification file to validate'),
|
|
29
62
|
});
|
|
30
|
-
const listLanguagesSchema = z.object({});
|
|
31
63
|
const getSpecInfoSchema = z.object({
|
|
32
64
|
specFilePath: z.string().describe('Path to the YAML specification file'),
|
|
33
65
|
});
|
|
34
|
-
const
|
|
35
|
-
const checkSpecSchema = z.object({
|
|
36
|
-
specFilePath: z.string().describe('Path to the YAML specification file to check for common mistakes'),
|
|
37
|
-
});
|
|
66
|
+
const listLanguagesSchema = z.object({});
|
|
38
67
|
// Type guards
|
|
39
68
|
function isRecord(value) {
|
|
40
69
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
70
|
}
|
|
42
71
|
function isSpecInfo(value) {
|
|
43
|
-
return
|
|
44
|
-
typeof value.version === 'string' &&
|
|
45
|
-
typeof value.title === 'string');
|
|
72
|
+
return isRecord(value) && typeof value.version === 'string' && typeof value.title === 'string';
|
|
46
73
|
}
|
|
47
74
|
function isExecError(error) {
|
|
48
75
|
return error instanceof Error;
|
|
49
76
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
typeof value.outputDirectory === 'string');
|
|
55
|
-
}
|
|
56
|
-
function isSpecFilePathArgs(value) {
|
|
57
|
-
return isRecord(value) && typeof value.specFilePath === 'string';
|
|
77
|
+
// Helper to read doc files
|
|
78
|
+
async function readDocFile(filename) {
|
|
79
|
+
const docPath = path.join(__dirname, 'docs', filename);
|
|
80
|
+
return fs.readFile(docPath, 'utf-8');
|
|
58
81
|
}
|
|
59
82
|
// Helper function to read YAML files
|
|
60
83
|
async function readYaml(filePath) {
|
|
@@ -66,14 +89,12 @@ function validateSpecData(data) {
|
|
|
66
89
|
if (!isRecord(data)) {
|
|
67
90
|
return { valid: false };
|
|
68
91
|
}
|
|
69
|
-
// Check for required info
|
|
70
92
|
if (!isRecord(data.info)) {
|
|
71
93
|
return { valid: false };
|
|
72
94
|
}
|
|
73
95
|
if (!isSpecInfo(data.info)) {
|
|
74
96
|
return { valid: false };
|
|
75
97
|
}
|
|
76
|
-
// At least one of types or groupedTypes must exist
|
|
77
98
|
const types = isRecord(data.types) ? data.types : undefined;
|
|
78
99
|
const groupedTypes = isRecord(data.groupedTypes) ? data.groupedTypes : undefined;
|
|
79
100
|
if (typeof types === 'undefined' && typeof groupedTypes === 'undefined') {
|
|
@@ -86,122 +107,235 @@ function validateSpecData(data) {
|
|
|
86
107
|
groupedTypes,
|
|
87
108
|
};
|
|
88
109
|
}
|
|
110
|
+
function checkSpecContent(specContent, resolvedSpecPath) {
|
|
111
|
+
const issues = [];
|
|
112
|
+
const warnings = [];
|
|
113
|
+
let isTopFile = false;
|
|
114
|
+
let fileType = 'UNKNOWN';
|
|
115
|
+
// Parse YAML to determine file type
|
|
116
|
+
let specData;
|
|
117
|
+
try {
|
|
118
|
+
specData = parseYaml(specContent);
|
|
119
|
+
if (!isRecord(specData)) {
|
|
120
|
+
issues.push('Spec file root is not an object. Expected YAML object at root level.');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
if (isRecord(specData.info) && isSpecInfo(specData.info)) {
|
|
124
|
+
isTopFile = true;
|
|
125
|
+
fileType = 'TOP FILE';
|
|
126
|
+
}
|
|
127
|
+
else if (isRecord(specData.info)) {
|
|
128
|
+
isTopFile = false;
|
|
129
|
+
fileType = 'INCOMPLETE TOP FILE';
|
|
130
|
+
if (typeof specData.info.version !== 'string') {
|
|
131
|
+
issues.push("Missing 'info.version' - Must be a string in semver format (e.g., '1.0.0').");
|
|
132
|
+
}
|
|
133
|
+
if (typeof specData.info.title !== 'string') {
|
|
134
|
+
issues.push("Missing 'info.title' - Must be a string describing the spec.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
isTopFile = false;
|
|
139
|
+
fileType = 'NON-TOP FILE';
|
|
140
|
+
}
|
|
141
|
+
const hasTypes = isRecord(specData.types);
|
|
142
|
+
const hasGroupedTypes = isRecord(specData.groupedTypes);
|
|
143
|
+
if (isTopFile && !hasTypes && !hasGroupedTypes) {
|
|
144
|
+
issues.push("Missing 'types' or 'groupedTypes' section - Top files must have at least one of these sections.");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (isExecError(error)) {
|
|
150
|
+
issues.push(`YAML parsing error: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
issues.push('YAML parsing error: Unable to parse spec file.');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const relativeFromCwd = path.relative(process.cwd(), resolvedSpecPath);
|
|
157
|
+
const suggestedPath = relativeFromCwd.startsWith('.') ? relativeFromCwd : `./${relativeFromCwd}`;
|
|
158
|
+
const lines = specContent.split('\n');
|
|
159
|
+
lines.forEach((line, index) => {
|
|
160
|
+
const lineNum = index + 1;
|
|
161
|
+
// Check for 'nullable: true'
|
|
162
|
+
if (line.match(/nullable\s*:\s*true/i)) {
|
|
163
|
+
issues.push(`Line ${lineNum}: Found 'nullable: true' - This property does NOT exist in Type Crafter. ` +
|
|
164
|
+
`Use the 'required' array to control nullability instead.`);
|
|
165
|
+
}
|
|
166
|
+
// Check for 'optional: true'
|
|
167
|
+
if (line.match(/optional\s*:\s*true/i)) {
|
|
168
|
+
issues.push(`Line ${lineNum}: Found 'optional: true' - This property does NOT exist in Type Crafter. ` +
|
|
169
|
+
`Use the 'required' array to control nullability instead.`);
|
|
170
|
+
}
|
|
171
|
+
// Check for property names with '?'
|
|
172
|
+
if (line.match(/^\s+[\w]+\?\s*:/)) {
|
|
173
|
+
issues.push(`Line ${lineNum}: Found property name with '?' suffix - This syntax is NOT supported. ` +
|
|
174
|
+
`Use the 'required' array instead.`);
|
|
175
|
+
}
|
|
176
|
+
// Check for type: [string, null] pattern
|
|
177
|
+
if (line.match(/type\s*:\s*\[.*,\s*null\]/)) {
|
|
178
|
+
issues.push(`Line ${lineNum}: Found 'type: [type, null]' pattern - This is NOT supported. ` +
|
|
179
|
+
`Use the 'required' array to control nullability instead.`);
|
|
180
|
+
}
|
|
181
|
+
// Check for top-level array types
|
|
182
|
+
if (line.match(/^\w+:\s*$/) && lines[index + 1]?.match(/^\s+type\s*:\s*array/)) {
|
|
183
|
+
warnings.push(`Line ${lineNum}: Possible top-level array type - Arrays cannot be top-level types. ` +
|
|
184
|
+
`They must be properties within objects.`);
|
|
185
|
+
}
|
|
186
|
+
// Check for '../' in $ref paths
|
|
187
|
+
if (line.match(/\$ref\s*:\s*['"].*\.\.\//)) {
|
|
188
|
+
issues.push(`Line ${lineNum}: Found relative path with '../' in $ref - Paths should be from project root, ` +
|
|
189
|
+
`not relative to the current file. Use './path/from/root/file.yaml#/Type' format.`);
|
|
190
|
+
}
|
|
191
|
+
// Check for # references in NON-TOP files
|
|
192
|
+
if (!isTopFile && fileType === 'NON-TOP FILE') {
|
|
193
|
+
const refMatch = line.match(/\$ref\s*:\s*['"]#\/([^'"]+)['"]/);
|
|
194
|
+
if (refMatch) {
|
|
195
|
+
const refPath = refMatch[1];
|
|
196
|
+
issues.push(`Line ${lineNum}: Found '#/${refPath}' reference in a NON-TOP FILE. ` +
|
|
197
|
+
`Non-top files (files without 'info' section) MUST use complete file paths for ALL references. ` +
|
|
198
|
+
`Use: $ref: '${suggestedPath}#/${refPath}'`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Check for missing './' prefix in external $ref
|
|
202
|
+
if (line.match(/\$ref\s*:\s*['"][^#'][^/]/)) {
|
|
203
|
+
const match = line.match(/\$ref\s*:\s*['"]([^'"]+)['"]/);
|
|
204
|
+
if (match && match[1] && !match[1].startsWith('#') && !match[1].startsWith('./')) {
|
|
205
|
+
warnings.push(`Line ${lineNum}: External $ref path should start with './' - ` +
|
|
206
|
+
`Use './path/from/root/file.yaml#/Type' format.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return { issues, warnings, isTopFile, fileType };
|
|
211
|
+
}
|
|
89
212
|
// Create server instance
|
|
90
213
|
const server = new McpServer({
|
|
91
|
-
name: 'type-crafter-mcp
|
|
92
|
-
version: '0.
|
|
214
|
+
name: 'type-crafter-mcp',
|
|
215
|
+
version: '0.2.0',
|
|
93
216
|
}, {
|
|
94
217
|
capabilities: {
|
|
95
218
|
tools: {},
|
|
96
219
|
},
|
|
97
220
|
});
|
|
98
|
-
//
|
|
99
|
-
server.registerTool('
|
|
100
|
-
description: '
|
|
101
|
-
'
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
221
|
+
// Tool 1: get-writing-guide
|
|
222
|
+
server.registerTool('get-writing-guide', {
|
|
223
|
+
description: 'Get the Type Crafter YAML specification writing guide. ' +
|
|
224
|
+
'CALL THIS FIRST before writing any YAML specs. ' +
|
|
225
|
+
'Returns a sessionId that should be passed to other tools. ' +
|
|
226
|
+
'The guide includes common mistakes to avoid, quick reference, and links to detailed sections.',
|
|
227
|
+
inputSchema: getWritingGuideSchema,
|
|
228
|
+
}, async () => {
|
|
229
|
+
try {
|
|
230
|
+
const sessionId = createSession();
|
|
231
|
+
const guideContent = await readDocFile('WRITING_GUIDE.md');
|
|
105
232
|
return {
|
|
106
233
|
content: [
|
|
107
234
|
{
|
|
108
235
|
type: 'text',
|
|
109
|
-
text:
|
|
236
|
+
text: `SESSION: ${sessionId}\n\n` +
|
|
237
|
+
`Save this sessionId and pass it to other Type Crafter tools.\n\n` +
|
|
238
|
+
`---\n\n${guideContent}`,
|
|
110
239
|
},
|
|
111
240
|
],
|
|
112
|
-
isError: true,
|
|
113
241
|
};
|
|
114
242
|
}
|
|
115
|
-
|
|
116
|
-
// Validate language
|
|
117
|
-
if (!['typescript', 'typescript-with-decoders'].includes(language.toLowerCase())) {
|
|
243
|
+
catch (error) {
|
|
118
244
|
return {
|
|
119
245
|
content: [
|
|
120
246
|
{
|
|
121
247
|
type: 'text',
|
|
122
|
-
text: `Error
|
|
248
|
+
text: `Error reading writing guide: ${isExecError(error) ? error.message : 'Unknown error'}`,
|
|
123
249
|
},
|
|
124
250
|
],
|
|
125
251
|
isError: true,
|
|
126
252
|
};
|
|
127
253
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
254
|
+
});
|
|
255
|
+
// Tool 2: get-rules-section
|
|
256
|
+
server.registerTool('get-rules-section', {
|
|
257
|
+
description: 'Get detailed rules for a specific topic. ' +
|
|
258
|
+
'Available sections: structure, types, nullable, references, composition, patterns. ' +
|
|
259
|
+
'Pass the sessionId from get-writing-guide for best experience.',
|
|
260
|
+
inputSchema: getRulesSectionSchema,
|
|
261
|
+
}, async (args) => {
|
|
262
|
+
const parsed = getRulesSectionSchema.safeParse(args);
|
|
263
|
+
if (!parsed.success) {
|
|
136
264
|
return {
|
|
137
265
|
content: [
|
|
138
266
|
{
|
|
139
267
|
type: 'text',
|
|
140
|
-
text:
|
|
268
|
+
text: 'Error: Invalid arguments. Required: section (structure|types|nullable|references|composition|patterns)',
|
|
141
269
|
},
|
|
142
270
|
],
|
|
143
271
|
isError: true,
|
|
144
272
|
};
|
|
145
273
|
}
|
|
146
|
-
|
|
147
|
-
|
|
274
|
+
const { sessionId, section } = parsed.data;
|
|
275
|
+
// Check session - warn if not provided but don't block
|
|
276
|
+
let sessionWarning = '';
|
|
277
|
+
if (!sessionId) {
|
|
278
|
+
sessionWarning =
|
|
279
|
+
'** Note: No sessionId provided. Call get-writing-guide first to get the basics and a sessionId.\n\n';
|
|
280
|
+
}
|
|
281
|
+
else if (!isValidSession(sessionId)) {
|
|
282
|
+
sessionWarning =
|
|
283
|
+
'** Note: Invalid or expired sessionId. Consider calling get-writing-guide again.\n\n';
|
|
284
|
+
}
|
|
285
|
+
const sectionFileMap = {
|
|
286
|
+
structure: 'RULES_STRUCTURE.md',
|
|
287
|
+
types: 'RULES_TYPES.md',
|
|
288
|
+
nullable: 'RULES_NULLABLE.md',
|
|
289
|
+
references: 'RULES_REFERENCES.md',
|
|
290
|
+
composition: 'RULES_COMPOSITION.md',
|
|
291
|
+
patterns: 'RULES_PATTERNS.md',
|
|
292
|
+
};
|
|
148
293
|
try {
|
|
149
|
-
const
|
|
150
|
-
const warningText = typeof stderr !== 'undefined' && stderr.length > 0 ? '\nWarnings:\n' + stderr : '';
|
|
294
|
+
const content = await readDocFile(sectionFileMap[section]);
|
|
151
295
|
return {
|
|
152
296
|
content: [
|
|
153
297
|
{
|
|
154
298
|
type: 'text',
|
|
155
|
-
text:
|
|
299
|
+
text: sessionWarning + content,
|
|
156
300
|
},
|
|
157
301
|
],
|
|
158
302
|
};
|
|
159
303
|
}
|
|
160
304
|
catch (error) {
|
|
161
|
-
if (!isExecError(error)) {
|
|
162
|
-
return {
|
|
163
|
-
content: [
|
|
164
|
-
{
|
|
165
|
-
type: 'text',
|
|
166
|
-
text: 'Error: Unknown error occurred during type generation',
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
isError: true,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
const errorDetails = error.stderr ?? error.stdout ?? '';
|
|
173
305
|
return {
|
|
174
306
|
content: [
|
|
175
307
|
{
|
|
176
308
|
type: 'text',
|
|
177
|
-
text: `Error
|
|
309
|
+
text: `Error reading section '${section}': ${isExecError(error) ? error.message : 'Unknown error'}`,
|
|
178
310
|
},
|
|
179
311
|
],
|
|
180
312
|
isError: true,
|
|
181
313
|
};
|
|
182
314
|
}
|
|
183
315
|
});
|
|
184
|
-
//
|
|
316
|
+
// Tool 3: validate-spec
|
|
185
317
|
server.registerTool('validate-spec', {
|
|
186
|
-
description: 'Validate a YAML specification file
|
|
187
|
-
'Checks
|
|
318
|
+
description: 'Validate a YAML specification file for correctness. ' +
|
|
319
|
+
'Checks structure AND common mistakes (nullable, optional, paths, etc.). ' +
|
|
320
|
+
'If validation fails with common mistakes, you likely need to read get-writing-guide first. ' +
|
|
321
|
+
'Pass sessionId from get-writing-guide for best experience.',
|
|
188
322
|
inputSchema: validateSpecSchema,
|
|
189
323
|
}, async (args) => {
|
|
190
|
-
|
|
324
|
+
const parsed = validateSpecSchema.safeParse(args);
|
|
325
|
+
if (!parsed.success) {
|
|
191
326
|
return {
|
|
192
327
|
content: [
|
|
193
328
|
{
|
|
194
329
|
type: 'text',
|
|
195
|
-
text: 'Error: Invalid arguments
|
|
330
|
+
text: 'Error: Invalid arguments. Required: specFilePath',
|
|
196
331
|
},
|
|
197
332
|
],
|
|
198
333
|
isError: true,
|
|
199
334
|
};
|
|
200
335
|
}
|
|
201
|
-
const { specFilePath } =
|
|
202
|
-
// Resolve path
|
|
336
|
+
const { sessionId, specFilePath } = parsed.data;
|
|
203
337
|
const resolvedSpecPath = path.resolve(specFilePath);
|
|
204
|
-
// Check if
|
|
338
|
+
// Check if file exists
|
|
205
339
|
try {
|
|
206
340
|
await fs.access(resolvedSpecPath);
|
|
207
341
|
}
|
|
@@ -216,68 +350,114 @@ server.registerTool('validate-spec', {
|
|
|
216
350
|
isError: true,
|
|
217
351
|
};
|
|
218
352
|
}
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
353
|
+
// Read spec content
|
|
354
|
+
let specContent;
|
|
355
|
+
try {
|
|
356
|
+
specContent = await fs.readFile(resolvedSpecPath, 'utf-8');
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
223
359
|
return {
|
|
224
360
|
content: [
|
|
225
361
|
{
|
|
226
362
|
type: 'text',
|
|
227
|
-
text:
|
|
363
|
+
text: `Error reading spec file: ${isExecError(error) ? error.message : 'Unknown error'}`,
|
|
228
364
|
},
|
|
229
365
|
],
|
|
230
366
|
isError: true,
|
|
231
367
|
};
|
|
232
368
|
}
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
369
|
+
// Run checks
|
|
370
|
+
const checkResult = checkSpecContent(specContent, resolvedSpecPath);
|
|
371
|
+
const fileName = path.basename(resolvedSpecPath);
|
|
372
|
+
let response = `File: ${fileName}\nType: ${checkResult.fileType}\n\n`;
|
|
373
|
+
// Add file type explanation
|
|
374
|
+
if (checkResult.fileType === 'TOP FILE') {
|
|
375
|
+
response += 'This is a TOP FILE (has info section).\n';
|
|
376
|
+
response += '- Can use #/types/TypeName for same-file references\n';
|
|
377
|
+
response += '- Can be used with type-crafter generate CLI\n\n';
|
|
378
|
+
}
|
|
379
|
+
else if (checkResult.fileType === 'NON-TOP FILE') {
|
|
380
|
+
response += 'This is a NON-TOP FILE (no info section).\n';
|
|
381
|
+
response += '- MUST use full paths for ALL references\n';
|
|
382
|
+
response += '- Must be referenced from a top file\n\n';
|
|
383
|
+
}
|
|
384
|
+
// Check if there are issues and no session - suggest reading guide
|
|
385
|
+
const hasCommonMistakes = checkResult.issues.some((i) => i.includes('nullable') ||
|
|
386
|
+
i.includes('optional') ||
|
|
387
|
+
i.includes("'?'") ||
|
|
388
|
+
i.includes('../') ||
|
|
389
|
+
i.includes('NON-TOP FILE'));
|
|
390
|
+
if (hasCommonMistakes && !isValidSession(sessionId)) {
|
|
391
|
+
response +=
|
|
392
|
+
'** RECOMMENDATION: These errors suggest you may not have read the writing guide.\n' +
|
|
393
|
+
'** Call get-writing-guide first to learn the correct YAML format.\n\n';
|
|
394
|
+
}
|
|
395
|
+
if (checkResult.issues.length === 0 && checkResult.warnings.length === 0) {
|
|
396
|
+
// Also validate structure
|
|
397
|
+
const specData = await readYaml(resolvedSpecPath);
|
|
398
|
+
const structureValidation = validateSpecData(specData);
|
|
399
|
+
if (structureValidation.valid && structureValidation.info) {
|
|
400
|
+
const typesCount = structureValidation.types
|
|
401
|
+
? Object.keys(structureValidation.types).length
|
|
402
|
+
: 0;
|
|
403
|
+
const groupedTypesCount = structureValidation.groupedTypes
|
|
404
|
+
? Object.keys(structureValidation.groupedTypes).length
|
|
405
|
+
: 0;
|
|
406
|
+
response += 'VALID - Specification file is valid!\n\n';
|
|
407
|
+
response += `Version: ${structureValidation.info.version}\n`;
|
|
408
|
+
response += `Title: ${structureValidation.info.title}\n`;
|
|
409
|
+
response += `Types: ${typesCount}\n`;
|
|
410
|
+
response += `Grouped Types: ${groupedTypesCount}\n\n`;
|
|
411
|
+
response += 'You can now run: type-crafter generate <language> <spec-path> <output-dir>';
|
|
412
|
+
return {
|
|
413
|
+
content: [{ type: 'text', text: response }],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (checkResult.issues.length > 0) {
|
|
418
|
+
response += 'ISSUES FOUND:\n\n';
|
|
419
|
+
checkResult.issues.forEach((issue, idx) => {
|
|
420
|
+
response += `${idx + 1}. ${issue}\n\n`;
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (checkResult.warnings.length > 0) {
|
|
424
|
+
response += 'WARNINGS:\n\n';
|
|
425
|
+
checkResult.warnings.forEach((warning, idx) => {
|
|
426
|
+
response += `${idx + 1}. ${warning}\n\n`;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (checkResult.issues.length > 0 || checkResult.warnings.length > 0) {
|
|
430
|
+
response += '\nFor detailed rules, call get-rules-section with the relevant topic:\n';
|
|
431
|
+
response += '- nullable: How to control nullability with required array\n';
|
|
432
|
+
response += '- references: $ref syntax and path rules\n';
|
|
433
|
+
response += '- types: Object, enum, array definitions\n';
|
|
434
|
+
response += '- structure: Top file vs non-top file rules\n';
|
|
435
|
+
}
|
|
251
436
|
return {
|
|
252
|
-
content: [
|
|
253
|
-
|
|
254
|
-
type: 'text',
|
|
255
|
-
text: 'Supported Languages:\n\n1. typescript - Generate TypeScript type definitions\n2. typescript-with-decoders - Generate TypeScript types with runtime decoders',
|
|
256
|
-
},
|
|
257
|
-
],
|
|
437
|
+
content: [{ type: 'text', text: response }],
|
|
438
|
+
isError: checkResult.issues.length > 0,
|
|
258
439
|
};
|
|
259
440
|
});
|
|
260
|
-
//
|
|
441
|
+
// Tool 4: get-spec-info
|
|
261
442
|
server.registerTool('get-spec-info', {
|
|
262
443
|
description: 'Get information about a YAML specification file including version, title, ' +
|
|
263
|
-
'and
|
|
444
|
+
'and all types defined in the spec.',
|
|
264
445
|
inputSchema: getSpecInfoSchema,
|
|
265
446
|
}, async (args) => {
|
|
266
|
-
|
|
447
|
+
const parsed = getSpecInfoSchema.safeParse(args);
|
|
448
|
+
if (!parsed.success) {
|
|
267
449
|
return {
|
|
268
450
|
content: [
|
|
269
451
|
{
|
|
270
452
|
type: 'text',
|
|
271
|
-
text: 'Error: Invalid arguments
|
|
453
|
+
text: 'Error: Invalid arguments. Required: specFilePath',
|
|
272
454
|
},
|
|
273
455
|
],
|
|
274
456
|
isError: true,
|
|
275
457
|
};
|
|
276
458
|
}
|
|
277
|
-
const { specFilePath } =
|
|
278
|
-
// Resolve path
|
|
459
|
+
const { specFilePath } = parsed.data;
|
|
279
460
|
const resolvedSpecPath = path.resolve(specFilePath);
|
|
280
|
-
// Check if spec file exists
|
|
281
461
|
try {
|
|
282
462
|
await fs.access(resolvedSpecPath);
|
|
283
463
|
}
|
|
@@ -292,7 +472,6 @@ server.registerTool('get-spec-info', {
|
|
|
292
472
|
isError: true,
|
|
293
473
|
};
|
|
294
474
|
}
|
|
295
|
-
// Read and validate the spec
|
|
296
475
|
const specFileData = await readYaml(resolvedSpecPath);
|
|
297
476
|
const validation = validateSpecData(specFileData);
|
|
298
477
|
if (!validation.valid || typeof validation.info === 'undefined') {
|
|
@@ -300,7 +479,7 @@ server.registerTool('get-spec-info', {
|
|
|
300
479
|
content: [
|
|
301
480
|
{
|
|
302
481
|
type: 'text',
|
|
303
|
-
text: 'Error: Invalid specification file format',
|
|
482
|
+
text: 'Error: Invalid specification file format. Missing info section or types/groupedTypes.',
|
|
304
483
|
},
|
|
305
484
|
],
|
|
306
485
|
isError: true,
|
|
@@ -339,243 +518,40 @@ server.registerTool('get-spec-info', {
|
|
|
339
518
|
});
|
|
340
519
|
}
|
|
341
520
|
return {
|
|
342
|
-
content: [
|
|
343
|
-
{
|
|
344
|
-
type: 'text',
|
|
345
|
-
text: infoText,
|
|
346
|
-
},
|
|
347
|
-
],
|
|
521
|
+
content: [{ type: 'text', text: infoText }],
|
|
348
522
|
};
|
|
349
523
|
});
|
|
350
|
-
//
|
|
351
|
-
server.registerTool('
|
|
352
|
-
description: '
|
|
353
|
-
|
|
354
|
-
'references, composition, best practices, and common patterns. Use this before creating or modifying spec files.',
|
|
355
|
-
inputSchema: getSpecRulesSchema,
|
|
524
|
+
// Tool 5: list-languages
|
|
525
|
+
server.registerTool('list-languages', {
|
|
526
|
+
description: 'List all supported target languages for type generation with the type-crafter CLI.',
|
|
527
|
+
inputSchema: listLanguagesSchema,
|
|
356
528
|
}, async () => {
|
|
357
|
-
try {
|
|
358
|
-
// Read the SPEC_RULES.md file from src directory
|
|
359
|
-
const rulesPath = path.join(__dirname, '..', 'src', 'SPEC_RULES.md');
|
|
360
|
-
const rulesContent = await fs.readFile(rulesPath, 'utf-8');
|
|
361
|
-
return {
|
|
362
|
-
content: [
|
|
363
|
-
{
|
|
364
|
-
type: 'text',
|
|
365
|
-
text: rulesContent,
|
|
366
|
-
},
|
|
367
|
-
],
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
if (!isExecError(error)) {
|
|
372
|
-
return {
|
|
373
|
-
content: [
|
|
374
|
-
{
|
|
375
|
-
type: 'text',
|
|
376
|
-
text: 'Error: Unknown error occurred while reading spec rules',
|
|
377
|
-
},
|
|
378
|
-
],
|
|
379
|
-
isError: true,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
return {
|
|
383
|
-
content: [
|
|
384
|
-
{
|
|
385
|
-
type: 'text',
|
|
386
|
-
text: `Error reading spec rules: ${error.message}`,
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
|
-
isError: true,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
// Register check-spec tool
|
|
394
|
-
server.registerTool('check-spec', {
|
|
395
|
-
description: 'Check a YAML specification file for common mistakes and provide helpful feedback. ' +
|
|
396
|
-
'This tool detects issues like using nullable/optional properties, incorrect array definitions, ' +
|
|
397
|
-
'wrong file paths, and other common errors. Use this before generating types to catch mistakes early.',
|
|
398
|
-
inputSchema: checkSpecSchema,
|
|
399
|
-
}, async (args) => {
|
|
400
|
-
if (!isSpecFilePathArgs(args)) {
|
|
401
|
-
return {
|
|
402
|
-
content: [
|
|
403
|
-
{
|
|
404
|
-
type: 'text',
|
|
405
|
-
text: 'Error: Invalid arguments provided',
|
|
406
|
-
},
|
|
407
|
-
],
|
|
408
|
-
isError: true,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
const { specFilePath } = args;
|
|
412
|
-
// Resolve path
|
|
413
|
-
const resolvedSpecPath = path.resolve(specFilePath);
|
|
414
|
-
// Check if spec file exists
|
|
415
|
-
try {
|
|
416
|
-
await fs.access(resolvedSpecPath);
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
return {
|
|
420
|
-
content: [
|
|
421
|
-
{
|
|
422
|
-
type: 'text',
|
|
423
|
-
text: `Error: Specification file not found at ${resolvedSpecPath}`,
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
isError: true,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
// Read the spec file
|
|
430
|
-
let specContent;
|
|
431
|
-
try {
|
|
432
|
-
specContent = await fs.readFile(resolvedSpecPath, 'utf-8');
|
|
433
|
-
}
|
|
434
|
-
catch (error) {
|
|
435
|
-
if (!isExecError(error)) {
|
|
436
|
-
return {
|
|
437
|
-
content: [
|
|
438
|
-
{
|
|
439
|
-
type: 'text',
|
|
440
|
-
text: 'Error: Unknown error occurred while reading spec file',
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
isError: true,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
return {
|
|
447
|
-
content: [
|
|
448
|
-
{
|
|
449
|
-
type: 'text',
|
|
450
|
-
text: `Error reading spec file: ${error.message}`,
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
isError: true,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
const issues = [];
|
|
457
|
-
const warnings = [];
|
|
458
|
-
// Check for common mistakes
|
|
459
|
-
const lines = specContent.split('\n');
|
|
460
|
-
lines.forEach((line, index) => {
|
|
461
|
-
const lineNum = index + 1;
|
|
462
|
-
// Check for 'nullable: true'
|
|
463
|
-
if (line.match(/nullable\s*:\s*true/i)) {
|
|
464
|
-
issues.push(`Line ${lineNum}: Found 'nullable: true' - This property does NOT exist in Type Crafter. ` +
|
|
465
|
-
`Use the 'required' array to control nullability instead.`);
|
|
466
|
-
}
|
|
467
|
-
// Check for 'optional: true'
|
|
468
|
-
if (line.match(/optional\s*:\s*true/i)) {
|
|
469
|
-
issues.push(`Line ${lineNum}: Found 'optional: true' - This property does NOT exist in Type Crafter. ` +
|
|
470
|
-
`Use the 'required' array to control optionality instead.`);
|
|
471
|
-
}
|
|
472
|
-
// Check for property names with '?'
|
|
473
|
-
if (line.match(/^\s+[\w]+\?\s*:/)) {
|
|
474
|
-
issues.push(`Line ${lineNum}: Found property name with '?' suffix - This syntax is NOT supported. ` +
|
|
475
|
-
`Use the 'required' array instead.`);
|
|
476
|
-
}
|
|
477
|
-
// Check for type: [string, null] pattern
|
|
478
|
-
if (line.match(/type\s*:\s*\[.*,\s*null\]/)) {
|
|
479
|
-
issues.push(`Line ${lineNum}: Found 'type: [type, null]' pattern - This is NOT supported. ` +
|
|
480
|
-
`Use the 'required' array to control nullability instead.`);
|
|
481
|
-
}
|
|
482
|
-
// Check for top-level array types (heuristic)
|
|
483
|
-
if (line.match(/^\w+:\s*$/) && lines[index + 1]?.match(/^\s+type\s*:\s*array/)) {
|
|
484
|
-
warnings.push(`Line ${lineNum}: Possible top-level array type - Arrays cannot be top-level types. ` +
|
|
485
|
-
`They must be properties within objects. Verify this is inside an object's properties.`);
|
|
486
|
-
}
|
|
487
|
-
// Check for '../' in $ref paths
|
|
488
|
-
if (line.match(/\$ref\s*:\s*['"].*\.\.\//)) {
|
|
489
|
-
issues.push(`Line ${lineNum}: Found relative path with '../' in $ref - Paths should be from project root, ` +
|
|
490
|
-
`not relative to the current file. Use './path/from/root/file.yaml#/Type' format.`);
|
|
491
|
-
}
|
|
492
|
-
// Check for missing './' prefix in external $ref
|
|
493
|
-
if (line.match(/\$ref\s*:\s*['"][^#'][^/]/)) {
|
|
494
|
-
const match = line.match(/\$ref\s*:\s*['"]([^'"]+)['"]/);
|
|
495
|
-
if (match && match[1] && !match[1].startsWith('#') && !match[1].startsWith('./')) {
|
|
496
|
-
warnings.push(`Line ${lineNum}: External $ref path should start with './' - ` +
|
|
497
|
-
`Use './path/from/root/file.yaml#/Type' format.`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
// Try to parse YAML and check for structural issues
|
|
502
|
-
try {
|
|
503
|
-
const specData = parseYaml(specContent);
|
|
504
|
-
if (!isRecord(specData)) {
|
|
505
|
-
issues.push('Spec file root is not an object. Expected YAML object at root level.');
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
// Check for missing info section
|
|
509
|
-
if (!isRecord(specData.info)) {
|
|
510
|
-
issues.push("Missing 'info' section - Every spec file must have an 'info' section with 'version' and 'title'.");
|
|
511
|
-
}
|
|
512
|
-
else if (!isSpecInfo(specData.info)) {
|
|
513
|
-
if (typeof specData.info.version !== 'string') {
|
|
514
|
-
issues.push("Missing 'info.version' - Must be a string in semver format (e.g., '1.0.0').");
|
|
515
|
-
}
|
|
516
|
-
if (typeof specData.info.title !== 'string') {
|
|
517
|
-
issues.push("Missing 'info.title' - Must be a string describing the spec.");
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
// Check if at least one of types or groupedTypes exists
|
|
521
|
-
const hasTypes = isRecord(specData.types);
|
|
522
|
-
const hasGroupedTypes = isRecord(specData.groupedTypes);
|
|
523
|
-
if (!hasTypes && !hasGroupedTypes) {
|
|
524
|
-
issues.push("Missing 'types' or 'groupedTypes' section - At least one must be defined.");
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
if (isExecError(error)) {
|
|
530
|
-
issues.push(`YAML parsing error: ${error.message}`);
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
issues.push('YAML parsing error: Unable to parse spec file.');
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
// Generate response
|
|
537
|
-
if (issues.length === 0 && warnings.length === 0) {
|
|
538
|
-
return {
|
|
539
|
-
content: [
|
|
540
|
-
{
|
|
541
|
-
type: 'text',
|
|
542
|
-
text: '✅ No common mistakes detected!\n\nThe spec file looks good. You can proceed with validation using validate-spec or generation using generate-types.',
|
|
543
|
-
},
|
|
544
|
-
],
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
let response = '';
|
|
548
|
-
if (issues.length > 0) {
|
|
549
|
-
response += '❌ Issues Found:\n\n';
|
|
550
|
-
issues.forEach((issue, idx) => {
|
|
551
|
-
response += `${idx + 1}. ${issue}\n\n`;
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
if (warnings.length > 0) {
|
|
555
|
-
if (issues.length > 0)
|
|
556
|
-
response += '\n';
|
|
557
|
-
response += '⚠️ Warnings:\n\n';
|
|
558
|
-
warnings.forEach((warning, idx) => {
|
|
559
|
-
response += `${idx + 1}. ${warning}\n\n`;
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
response +=
|
|
563
|
-
'\n📖 For detailed rules and examples, use the get-spec-rules tool to see complete documentation.';
|
|
564
529
|
return {
|
|
565
530
|
content: [
|
|
566
531
|
{
|
|
567
532
|
type: 'text',
|
|
568
|
-
text:
|
|
533
|
+
text: 'Supported Languages for type-crafter generate:\n\n' +
|
|
534
|
+
'1. typescript\n' +
|
|
535
|
+
' - Generates TypeScript type definitions (.ts files)\n' +
|
|
536
|
+
' - Usage: type-crafter generate typescript <spec.yaml> <output-dir>\n\n' +
|
|
537
|
+
'2. typescript-with-decoders\n' +
|
|
538
|
+
' - Generates TypeScript types WITH runtime decoders\n' +
|
|
539
|
+
' - Useful for runtime validation of API responses\n' +
|
|
540
|
+
' - Usage: type-crafter generate typescript-with-decoders <spec.yaml> <output-dir>\n\n' +
|
|
541
|
+
'Writer Modes:\n' +
|
|
542
|
+
'- typesWriterMode: SingleFile | Files\n' +
|
|
543
|
+
'- groupedTypesWriterMode: FolderWithFiles | SingleFile\n\n' +
|
|
544
|
+
'Example:\n' +
|
|
545
|
+
'type-crafter generate typescript ./types.yaml ./src/types SingleFile FolderWithFiles',
|
|
569
546
|
},
|
|
570
547
|
],
|
|
571
|
-
isError: issues.length > 0,
|
|
572
548
|
};
|
|
573
549
|
});
|
|
574
550
|
// Start the server
|
|
575
551
|
async function main() {
|
|
576
552
|
const transport = new StdioServerTransport();
|
|
577
553
|
await server.connect(transport);
|
|
578
|
-
console.error('Type Crafter MCP Server running on stdio');
|
|
554
|
+
console.error('Type Crafter MCP Server v0.2.0 running on stdio');
|
|
579
555
|
}
|
|
580
556
|
main().catch((error) => {
|
|
581
557
|
console.error('Server error:', error);
|