@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/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 { exec } from 'child_process';
8
- import { promisify } from 'util';
9
+ import { randomUUID } from 'crypto';
9
10
  import { z } from 'zod';
10
- const execAsync = promisify(exec);
11
- // Zod schemas for all tools
12
- const generateTypesSchema = z.object({
13
- language: z
14
- .enum(['typescript', 'typescript-with-decoders'])
15
- .describe('Target language for type generation'),
16
- specFilePath: z.string().describe('Path to the YAML specification file'),
17
- outputDirectory: z.string().describe('Directory where generated types will be written'),
18
- typesWriterMode: z
19
- .enum(['SingleFile', 'Files'])
20
- .optional()
21
- .describe('Writer mode for types: SingleFile or Files'),
22
- groupedTypesWriterMode: z
23
- .enum(['FolderWithFiles', 'SingleFile'])
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('Writer mode for grouped types: FolderWithFiles or SingleFile'),
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 getSpecRulesSchema = z.object({});
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 (isRecord(value) &&
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
- function isGenerateTypesArgs(value) {
51
- return (isRecord(value) &&
52
- typeof value.language === 'string' &&
53
- typeof value.specFilePath === 'string' &&
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-server',
92
- version: '0.1.0',
214
+ name: 'type-crafter-mcp',
215
+ version: '0.2.0',
93
216
  }, {
94
217
  capabilities: {
95
218
  tools: {},
96
219
  },
97
220
  });
98
- // Register generate-types tool
99
- server.registerTool('generate-types', {
100
- description: 'Generate type definitions from a YAML specification file. Supports TypeScript and TypeScript with decoders. ' +
101
- 'The YAML spec should follow the Type Crafter format with info, types, and/or groupedTypes sections.',
102
- inputSchema: generateTypesSchema,
103
- }, async (args) => {
104
- if (!isGenerateTypesArgs(args)) {
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: 'Error: Invalid arguments provided',
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
- const { language, specFilePath, outputDirectory, typesWriterMode = 'SingleFile', groupedTypesWriterMode = 'SingleFile', } = args;
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: Unsupported language "${language}". Supported languages: typescript, typescript-with-decoders`,
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
- // Resolve paths
129
- const resolvedSpecPath = path.resolve(specFilePath);
130
- const resolvedOutputPath = path.resolve(outputDirectory);
131
- // Check if spec file exists
132
- try {
133
- await fs.access(resolvedSpecPath);
134
- }
135
- catch {
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: `Error: Specification file not found at ${resolvedSpecPath}`,
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
- // Use the type-crafter CLI to generate types
147
- const cliCommand = `type-crafter generate ${language} "${resolvedSpecPath}" "${resolvedOutputPath}" ${typesWriterMode} ${groupedTypesWriterMode}`;
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 { stdout, stderr } = await execAsync(cliCommand);
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: `Successfully generated ${language} types from ${specFilePath}\nOutput saved to: ${outputDirectory}\n\n${stdout}${warningText}`,
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 generating types: ${error.message}\n${errorDetails}`,
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
- // Register validate-spec tool
316
+ // Tool 3: validate-spec
185
317
  server.registerTool('validate-spec', {
186
- description: 'Validate a YAML specification file without generating types. ' +
187
- 'Checks if the spec file is valid and can be processed by Type Crafter.',
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
- if (!isSpecFilePathArgs(args)) {
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 provided',
330
+ text: 'Error: Invalid arguments. Required: specFilePath',
196
331
  },
197
332
  ],
198
333
  isError: true,
199
334
  };
200
335
  }
201
- const { specFilePath } = args;
202
- // Resolve path
336
+ const { sessionId, specFilePath } = parsed.data;
203
337
  const resolvedSpecPath = path.resolve(specFilePath);
204
- // Check if spec file exists
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
- // Try to read and validate the spec
220
- const specFileData = await readYaml(resolvedSpecPath);
221
- const validation = validateSpecData(specFileData);
222
- if (!validation.valid || typeof validation.info === 'undefined') {
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: 'Error: Invalid specification file. Neither types nor groupedTypes found!',
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
- const typesCount = typeof validation.types !== 'undefined' ? Object.keys(validation.types).length : 0;
234
- const groupedTypesCount = typeof validation.groupedTypes !== 'undefined'
235
- ? Object.keys(validation.groupedTypes).length
236
- : 0;
237
- return {
238
- content: [
239
- {
240
- type: 'text',
241
- text: `✓ Specification file is valid!\n\nInfo:\n Version: ${validation.info.version}\n Title: ${validation.info.title}\n Types: ${typesCount}\n Grouped Types: ${groupedTypesCount}`,
242
- },
243
- ],
244
- };
245
- });
246
- // Register list-languages tool
247
- server.registerTool('list-languages', {
248
- description: 'List all supported target languages for type generation',
249
- inputSchema: listLanguagesSchema,
250
- }, async () => {
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
- // Register get-spec-info tool
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 counts of types and grouped types defined in the spec.',
444
+ 'and all types defined in the spec.',
264
445
  inputSchema: getSpecInfoSchema,
265
446
  }, async (args) => {
266
- if (!isSpecFilePathArgs(args)) {
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 provided',
453
+ text: 'Error: Invalid arguments. Required: specFilePath',
272
454
  },
273
455
  ],
274
456
  isError: true,
275
457
  };
276
458
  }
277
- const { specFilePath } = args;
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
- // Register get-spec-rules tool
351
- server.registerTool('get-spec-rules', {
352
- description: 'Get comprehensive rules and guidelines for writing Type Crafter YAML specification files. ' +
353
- 'This provides LLMs with detailed information about the YAML spec format, type mappings, nullable types, ' +
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: response,
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);