Skip to content

Instantly share code, notes, and snippets.

@brianpos
Created April 7, 2025 08:18
Show Gist options
  • Save brianpos/81472794b528183e40b657aad3ec921c to your computer and use it in GitHub Desktop.
Save brianpos/81472794b528183e40b657aad3ec921c to your computer and use it in GitHub Desktop.
Extract from a POC demonstrating an MCP Server for FHIRPath
extern alias r4;
using r4.Hl7.Fhir.Model;
using Hl7.Fhir.Model;
using Hl7.Fhir.StructuredDataCapture;
using Hl7.Fhir.Utility;
using Hl7.FhirPath;
using Hl7.FhirPath.Expressions;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using UploadFIG;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Hl7.Fhir.Model.CdsHooks;
[McpServerToolType]
public static class FhirPathTools
{
[McpServerTool, System.ComponentModel.Description("Parse a fhirpath expression then reformat it into its canonical form. Expressions that are invalid with respect to the grammar will return an error. However validation should be done using ValidateFhirPath or ValidateFhirPathWithContext.")]
public static string CanonicalFhirPath(string expression)
{
FhirPathCompiler compiler = new FhirPathCompiler();
try
{
var ast = compiler.Parse(expression);
CanonicalVisitor visitor = new CanonicalVisitor();
var result = ast.Accept(visitor);
var formatted = result.ToString();
StringBuilder sb = new StringBuilder();
sb.AppendLine("The canonical representation of this fhirpath expression is:");
sb.AppendLine("```fhirpath");
sb.AppendLine(formatted);
sb.AppendLine("```");
return sb.ToString();
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
[McpServerTool, System.ComponentModel.Description("Validates a fhirpath expression and return the expected datatype of the result.")]
public static string ValidateFhirPath(string expression)
{
return ValidateFhirPathWithContext(expression, null);
}
[McpServerTool, System.ComponentModel.Description("Validates a fhirpath `expression` given a `context` of either ResourceType (or ResourceType.path) and return the expected datatype of the result.")]
public static string ValidateFhirPathWithContext(string expression, string context)
{
UploadFIG.fhirVersion? FhirVersion = UploadFIG.fhirVersion.R4;
FhirPathCompiler compiler = new FhirPathCompiler();
try
{
var ast = compiler.Parse(expression);
CanonicalVisitor visitor = new CanonicalVisitor();
var result = ast.Accept(visitor);
var formatted = result.ToString();
Common_Processor versionAgnosticProcessor = null;
ExpressionValidator? expressionValidator = null;
if (FhirVersion == UploadFIG.fhirVersion.R4)
{
versionAgnosticProcessor = new R4_Processor();
expressionValidator = new ExpressionValidatorR4(versionAgnosticProcessor, false);
}
else if (FhirVersion == UploadFIG.fhirVersion.R4B)
{
versionAgnosticProcessor = new R4B_Processor();
expressionValidator = new ExpressionValidatorR4B(versionAgnosticProcessor, false);
}
else if (FhirVersion == UploadFIG.fhirVersion.R5)
{
versionAgnosticProcessor = new R5_Processor();
expressionValidator = new ExpressionValidatorR5(versionAgnosticProcessor, false);
}
else
{
return "Not possible";
}
var visitor2 = expressionValidator.CreateFhirPathValidator();
if (!string.IsNullOrEmpty(context))
visitor2.SetContext(context);
var pe = compiler.Parse(expression);
var r = pe.Accept(visitor2);
StringBuilder sb = new StringBuilder();
sb.AppendLine("The canonical representation of this fhirpath expression is:");
sb.AppendLine("```fhirpath");
sb.AppendLine(formatted);
sb.AppendLine("```");
sb.AppendLine($"Return type: {r.ToString()}");
if (!visitor2.Outcome.Success)
{
sb.AppendLine("Error(s) validating expression:");
ReportOutcomeMessages(visitor2.Outcome, sb);
}
else if (visitor2.Outcome.Warnings > 0)
{
sb.AppendLine("Warning(s) validating expression:");
ReportOutcomeMessages(visitor2.Outcome, sb);
}
else if (visitor2.Outcome.Issue.Any(i => i.Severity == OperationOutcome.IssueSeverity.Information))
{
sb.AppendLine("Informational messages encountered while validating expression");
ReportOutcomeMessages(visitor2.Outcome, sb);
}
return sb.ToString();
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
const string diagnosticPrefix = " ";
private static void ReportOutcomeMessages(OperationOutcome outcome, StringBuilder sb)
{
foreach (var issue in outcome.Issue)
{
sb.AppendLine($" --> {issue.Severity?.GetLiteral()}: {issue.Details.Text}");
if (!string.IsNullOrEmpty(issue.Diagnostics))
{
var diag = issue.Diagnostics.Replace("\r\n\r\n", "\r\n").Trim();
sb.AppendLine($"{diagnosticPrefix}{diag.Replace("\r\n", "\r\n " + diagnosticPrefix)}");
}
}
}
static Dictionary<string, Function> FhirPathFunctions = new Dictionary<string, Function>();
[McpServerTool, System.ComponentModel.Description("Retrieve a list of all fhirpath function from the latest version of the specification given its name")]
public static string ListAllFunctions()
{
InitializeDictionary();
StringBuilder sb = new StringBuilder();
sb.AppendLine("This is the complete list of fhirpath functions:");
foreach (var function in FhirPathFunctions.Values)
{
sb.Append($"* {function.FunctionName}({string.Join(", ", function.Arguments.Select(a => a.Key))})");
if (!string.IsNullOrEmpty(function.ReturnType))
sb.Append($" : {function.ReturnType}");
sb.AppendLine();
}
return sb.ToString();
}
[McpServerTool, System.ComponentModel.Description("Provides a detailed definition of a fhirpath function from the latest version of the specification given its name")]
public static string ReadFunctionDetails(string functionName)
{
InitializeDictionary();
StringBuilder sb = new StringBuilder();
if (FhirPathFunctions.TryGetValue(functionName, out var function))
{
ExtractFunctionDetails(sb, function);
}
else
{
sb.AppendLine($"Function {functionName} was not found among my function definitions.");
}
return sb.ToString();
}
private static void InitializeDictionary()
{
if (FhirPathFunctions.Count == 0)
{
// Load in the expressions from the json content
string jsonFilePath = "functions.json";
string jsonString = File.ReadAllText(jsonFilePath);
FunctionsJson functionsJson = JsonSerializer.Deserialize<FunctionsJson>(jsonString);
// Now you can access the functions
foreach (var function in functionsJson.Functions)
{
FhirPathFunctions.Add(function.FunctionName, function);
// Console.WriteLine($"Function Name: {function.FunctionName}");
// Access other properties as needed
}
}
}
public static string ReadAllFunctionDetails()
{
InitializeDictionary();
StringBuilder sb = new StringBuilder();
foreach (var function in FhirPathFunctions.Values)
{
ExtractFunctionDetails(sb, function);
}
// return sb.ToString();
var css = """
<style>
pre:has(.language-fhirpath) {
border-left: solid 2pt green;
padding: 12px;
background-color: bisque;
}
pre code {
background-color: unset;
padding: unset;
}
code {
background-color: bisque;
border-radius: 4px;
padding: 4px;
}
</style>
""";
return css + Markdig.Markdown.ToHtml(sb.ToString());
}
private static void ExtractFunctionDetails(StringBuilder sb, Function function)
{
sb.Append($"### {function.SectionNumber} {function.FunctionName}({string.Join(", ", function.Arguments.Keys.Select(k => k))}");
sb.AppendLine($"): {function.ReturnType}");
sb.AppendLine(function.Description);
sb.AppendLine();
if (!string.IsNullOrEmpty(function.Status))
{
sb.AppendLine("Standards status: " + function.Status);
sb.AppendLine();
}
sb.AppendLine($"Input collection cardinality: 0..{(function.ErrorOnMultipleInput ? "1" : "*")}");
sb.AppendLine($"Input collection empty returns: {function.EmptyInputResult}");
if (function.InputTypes.Count > 0)
{
sb.AppendLine($"Input collection types: {string.Join(", ", function.InputTypes)}");
}
if (function.Arguments?.Any() == true)
{
sb.AppendLine("#### *Parameters*");
foreach (var arg in function.Arguments)
{
sb.AppendLine($"* `{arg.Key}: {arg.Value.Type}` // {arg.Value.Description}");
}
}
sb.AppendLine("#### *Example of use*");
sb.AppendLine(function.ExampleOfUse?.Replace("HINT: ", "*(AI generated example)*\n"));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment