Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/fmlrunner-mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,20 @@ export class FmlRunnerMcp {
},
required: ['valueSetRef', 'code']
}
},
{
name: 'validate-fml-syntax',
description: 'Validate FML (FHIR Mapping Language) syntax without compiling to StructureMap',
inputSchema: {
type: 'object',
properties: {
fmlContent: {
type: 'string',
description: 'FML content to validate for syntax errors'
}
},
required: ['fmlContent']
}
}
]
};
Expand Down Expand Up @@ -349,6 +363,9 @@ export class FmlRunnerMcp {
case 'validate-code':
return await this.handleValidateCode(args);

case 'validate-fml-syntax':
return await this.handleValidateFmlSyntax(args);

default:
throw new Error(`Unknown tool: ${name}`);
}
Expand Down Expand Up @@ -585,6 +602,54 @@ export class FmlRunnerMcp {
};
}

private async handleValidateFmlSyntax(args: any): Promise<CallToolResult> {
const { fmlContent } = args;

const result = this.fmlRunner.validateFmlSyntax(fmlContent);

if (!result.success) {
return {
content: [
{
type: 'text',
text: `Validation service error: ${result.errors?.join(', ') || 'Unknown error'}`
}
],
isError: true
};
}

if (result.valid) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
valid: true,
message: 'FML syntax is valid'
}, null, 2)
}
]
};
} else {
return {
content: [
{
type: 'text',
text: JSON.stringify({
valid: false,
errors: result.errors || [],
warnings: result.warnings || [],
lineNumber: result.lineNumber,
columnNumber: result.columnNumber,
errorLocation: result.errorLocation
}, null, 2)
}
]
};
}
}

/**
* Start the MCP server
*/
Expand Down
80 changes: 79 additions & 1 deletion packages/fmlrunner-rest/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,82 @@ paths:
schema:
$ref: '#/components/schemas/OperationOutcome'

/validate-fml-syntax:
post:
summary: Validate FML syntax
description: |
Validate the syntax of FHIR Mapping Language (FML) content without compiling to StructureMap.
Returns detailed error information including line numbers and error locations for invalid syntax.
tags:
- Validation
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
fmlContent:
type: string
description: FML content to validate for syntax errors
example: |
map "http://example.org/TestMapping" = "TestMapping"

uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse" alias QR as source
uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as target

group QuestionnaireResponse(source src : QR, target tgt : Patient) {
src.item as item -> tgt.gender = 'unknown';
}
required:
- fmlContent
responses:
'200':
description: FML syntax is valid
content:
application/json:
schema:
$ref: '#/components/schemas/OperationOutcome'
example:
resourceType: OperationOutcome
issue:
- severity: information
code: informational
diagnostics: FML syntax is valid
'400':
description: FML syntax is invalid or request is malformed
content:
application/json:
schema:
$ref: '#/components/schemas/OperationOutcome'
examples:
syntax_error:
summary: Syntax validation error
value:
resourceType: OperationOutcome
issue:
- severity: error
code: invariant
diagnostics: "FML content must start with a map declaration (e.g., \"map \\\"url\\\" = \\\"name\\\")"
location:
- "Beginning of file"
expression:
- "line 1"
missing_content:
summary: Missing content error
value:
resourceType: OperationOutcome
issue:
- severity: error
code: invalid
diagnostics: fmlContent is required in request body
'500':
description: Internal server error during validation
content:
application/json:
schema:
$ref: '#/components/schemas/OperationOutcome'

components:
schemas:
StructureMap:
Expand Down Expand Up @@ -517,4 +593,6 @@ tags:
- name: StructureMap Operations
description: FHIR StructureMap operations
- name: Bundle
description: FHIR Bundle processing
description: FHIR Bundle processing
- name: Validation
description: FML syntax and content validation
78 changes: 78 additions & 0 deletions packages/fmlrunner-rest/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export class FmlRunnerApi {
// Validation endpoint
apiRouter.post('/validate', this.validateResource.bind(this));

// FML syntax validation endpoint
apiRouter.post('/validate-fml-syntax', this.validateFmlSyntax.bind(this));

// Health check endpoint
apiRouter.get('/health', this.healthCheck.bind(this));

Expand Down Expand Up @@ -743,6 +746,81 @@ export class FmlRunnerApi {
}
}

/**
* Validate FML syntax
*/
private async validateFmlSyntax(req: Request, res: Response): Promise<void> {
try {
const { fmlContent } = req.body;

if (!fmlContent) {
res.status(400).json({
resourceType: 'OperationOutcome',
issue: [{
severity: 'error',
code: 'invalid',
diagnostics: 'fmlContent is required in request body'
}]
});
return;
}

const validationResult = this.fmlRunner.validateFmlSyntax(fmlContent);

if (!validationResult.success) {
res.status(500).json({
resourceType: 'OperationOutcome',
issue: [{
severity: 'error',
code: 'exception',
diagnostics: validationResult.errors?.join(', ') || 'Validation service failed'
}]
});
return;
}

if (validationResult.valid) {
res.json({
resourceType: 'OperationOutcome',
issue: [{
severity: 'information',
code: 'informational',
diagnostics: 'FML syntax is valid'
}]
});
} else {
const issues = (validationResult.errors || []).map(error => ({
severity: 'error' as const,
code: 'invariant' as const,
diagnostics: error,
location: validationResult.errorLocation ? [validationResult.errorLocation] : undefined,
expression: validationResult.lineNumber ? [`line ${validationResult.lineNumber}`] : undefined
}));

// Add warnings if any
const warningIssues = (validationResult.warnings || []).map(warning => ({
severity: 'warning' as const,
code: 'invariant' as const,
diagnostics: warning
}));

res.status(400).json({
resourceType: 'OperationOutcome',
issue: [...issues, ...warningIssues]
});
}
} catch (error) {
res.status(500).json({
resourceType: 'OperationOutcome',
issue: [{
severity: 'error',
code: 'exception',
diagnostics: error instanceof Error ? error.message : 'Unknown error during FML syntax validation'
}]
});
}
}

/**
* Health check endpoint
*/
Expand Down
18 changes: 18 additions & 0 deletions packages/fmlrunner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SchemaValidator } from './lib/schema-validator';
import {
StructureMap,
FmlCompilationResult,
FmlSyntaxValidationResult,
ExecutionResult,
EnhancedExecutionResult,
ExecutionOptions,
Expand Down Expand Up @@ -121,6 +122,23 @@ export class FmlRunner {
return result;
}

/**
* Validate FML syntax without generating StructureMap
*/
validateFmlSyntax(fmlContent: string): FmlSyntaxValidationResult {
this.logger.debug('Validating FML syntax', { contentLength: fmlContent.length });

const result = this.compiler.validateSyntax(fmlContent);

this.logger.info('FML syntax validation completed', {
success: result.success,
valid: result.valid,
errorCount: result.errors?.length || 0
});

return result;
}

/**
* Execute StructureMap on input content with validation
*/
Expand Down
Loading