Feature/cds config (#857)

* Added MCP support using SSE on http://localhost:8080/sse

* Reverted change that IntelliJ complains about

* Pre-rework

* Cleaned up the code a fair bit

* Renamed

* Renamed

* Running spotless

* Reuse FhirContext in result serialization to make MCP server work with R5

* Added support for transactions

* PoC tool for CDS Hooks

* some cleanup

* Upgrade of model protocol

* Added comments

* Removed field injection ... CDS to be changed to AutoConfig eventually

* Adjusted to new builder pattern

* Update src/main/java/ca/uhn/fhir/rest/server/MCPBridge.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* A bit of restructuring

* More rework

* Removing (suspected unnecessary) formatting

* Add more example doc

* Added a smoke- / passthrough-test

* Applied spotless

* Update src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/jpa/starter/mcp/ToolFactory.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Formatting

* Added some documentation

* spotless cares about MD?

* Reverting back to default values

* minor refinements

* Fixed CDS hooks configuration

* Fixed some wirings

* Readded missing elements

* getting closer to get test running again ...

* applying review

* Readded exclude

* Bumped spring-ai deps

* added agents file

* Updated according to review

---------

Co-authored-by: Ádám Z. Kövér <adamzkover@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jens Kristian Villadsen
2025-10-01 22:17:07 +02:00
committed by GitHub
parent d29b9f80af
commit d76662c9e9
20 changed files with 568 additions and 537 deletions

View File

@@ -1,7 +1,9 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cdshooks")
public class CdsHooksProperties {

View File

@@ -2,7 +2,6 @@ package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.jpa.starter.AppProperties;
import ca.uhn.fhir.rest.api.server.cdshooks.CdsServiceRequestJson;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
@@ -43,9 +42,6 @@ public class CdsHooksServlet extends HttpServlet {
@Autowired
ICdsServiceRegistry cdsServiceRegistry;
@Autowired
RestfulServer restfulServer;
@Autowired
@Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY)
ObjectMapper objectMapper;

View File

@@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.jpa.starter.cr.CrProperties;
import ca.uhn.fhir.jpa.starter.cr.CqlRuntimeProperties;
public class ProviderConfiguration {
private final String clientIdHeaderName;
@@ -11,8 +11,8 @@ public class ProviderConfiguration {
this.clientIdHeaderName = clientIdHeaderName;
}
public ProviderConfiguration(CdsHooksProperties cdsProperties, CrProperties crProperties) {
this(crProperties.getCql().getRuntime().isDebugLoggingEnabled(), cdsProperties.getClientIdHeaderName());
public ProviderConfiguration(CdsHooksProperties cdsProperties, CqlRuntimeProperties cqlRuntimeProperties) {
this(cqlRuntimeProperties.isDebugLoggingEnabled(), cdsProperties.getClientIdHeaderName());
}
public String getClientIdHeaderName() {

View File

@@ -1,20 +1,13 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.starter.cr.CrCommonConfig;
import ca.uhn.fhir.jpa.starter.cr.CrConfigCondition;
import ca.uhn.fhir.jpa.starter.cr.CrProperties;
import ca.uhn.fhir.jpa.starter.cr.*;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
import ca.uhn.hapi.fhir.cdshooks.config.CdsHooksConfig;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsHooksContextBooter;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.CdsCrServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.CdsCrSettings;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.ICdsCrServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.discovery.CdsCrDiscoveryServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.discovery.ICdsCrDiscoveryServiceRegistry;
import org.opencds.cqf.fhir.cr.hapi.config.CrCdsHooksConfig;
import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig;
import org.opencds.cqf.fhir.cr.hapi.config.test.TestCdsHooksConfig;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
@@ -24,30 +17,9 @@ import org.springframework.context.annotation.Import;
@Configuration
@Conditional({CdsHooksConfigCondition.class, CrConfigCondition.class})
@Import({RepositoryConfig.class, TestCdsHooksConfig.class, CrCdsHooksConfig.class, CrCommonConfig.class})
@Import({RepositoryConfig.class, CrCdsHooksConfig.class, CrCommonConfig.class, CdsHooksConfig.class})
public class StarterCdsHooksConfig {
@Bean
public ICdsCrDiscoveryServiceRegistry cdsCrDiscoveryServiceRegistry() {
CdsCrDiscoveryServiceRegistry registry = new CdsCrDiscoveryServiceRegistry();
registry.unregister(FhirVersionEnum.R4);
registry.register(FhirVersionEnum.R4, UpdatedCrDiscoveryService.class);
return registry;
}
@Bean
public ICdsCrServiceRegistry cdsCrServiceRegistry() {
CdsCrServiceRegistry registry = new CdsCrServiceRegistry();
registry.unregister(FhirVersionEnum.R4);
registry.register(FhirVersionEnum.R4, UpdatedCdsCrService.class);
return registry;
}
@Bean
public CdsHooksProperties cdsHooksProperties() {
return new CdsHooksProperties();
}
@Bean
public CdsCrSettings cdsCrSettings(CdsHooksProperties cdsHooksProperties) {
CdsCrSettings settings = CdsCrSettings.getDefault();
@@ -67,8 +39,9 @@ public class StarterCdsHooksConfig {
}
@Bean
public ProviderConfiguration providerConfiguration(CdsHooksProperties cdsProperties, CrProperties crProperties) {
return new ProviderConfiguration(cdsProperties, crProperties);
public ProviderConfiguration providerConfiguration(
CdsHooksProperties cdsProperties, CqlProperties cqlProperties, CqlRuntimeProperties cqlRuntimeProperties) {
return new ProviderConfiguration(cdsProperties, cqlRuntimeProperties);
}
@Bean

View File

@@ -1,42 +0,0 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.repository.IRepository;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.cdshooks.CdsServiceRequestJson;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.CdsCrService;
import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory;
import static org.opencds.cqf.fhir.utility.Constants.APPLY_PARAMETER_DATA;
public class UpdatedCdsCrService extends CdsCrService {
private final IAdapterFactory adapterFactory;
public UpdatedCdsCrService(RequestDetails theRequestDetails, IRepository theRepository) {
super(theRequestDetails, theRepository);
adapterFactory = IAdapterFactory.forFhirContext(theRepository.fhirContext());
}
@Override
public IBaseParameters encodeParams(CdsServiceRequestJson theJson) {
var parameters = adapterFactory.createParameters(super.encodeParams(theJson));
if (parameters.hasParameter(APPLY_PARAMETER_DATA)) {
parameters.addParameter(
"useServerData",
booleanTypeForVersion(parameters.fhirContext().getVersion().getVersion(), false));
}
return (IBaseParameters) parameters.get();
}
private IPrimitiveType<Boolean> booleanTypeForVersion(FhirVersionEnum fhirVersion, boolean value) {
return switch (fhirVersion) {
case DSTU2 -> new org.hl7.fhir.dstu2.model.BooleanType(value);
case DSTU3 -> new org.hl7.fhir.dstu3.model.BooleanType(value);
case R4 -> new org.hl7.fhir.r4.model.BooleanType(value);
case R5 -> new org.hl7.fhir.r5.model.BooleanType(value);
default -> throw new IllegalArgumentException("unknown or unsupported FHIR version");
};
}
}

View File

@@ -1,12 +0,0 @@
package ca.uhn.fhir.jpa.starter.cdshooks;
import ca.uhn.fhir.repository.IRepository;
import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.fhir.cr.hapi.cdshooks.discovery.CrDiscoveryService;
public class UpdatedCrDiscoveryService extends CrDiscoveryService {
public UpdatedCrDiscoveryService(IIdType thePlanDefinitionId, IRepository theRepository) {
super(thePlanDefinitionId, theRepository);
maxUriLength = 6000;
}
}

View File

@@ -1,5 +1,10 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.caregaps")
public class CareGapsProperties {
private String reporter = "default";
private String section_author = "default";

View File

@@ -3,7 +3,11 @@ package ca.uhn.fhir.jpa.starter.cr;
import org.cqframework.cql.cql2elm.CqlCompilerException;
import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.cql2elm.LibraryBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.compiler")
public class CqlCompilerProperties {
private Boolean validate_units = true;
private Boolean verify_only = false;

View File

@@ -0,0 +1,46 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.data")
public class CqlData {
private RetrieveSettings.SEARCH_FILTER_MODE searchParameterMode = RetrieveSettings.SEARCH_FILTER_MODE.AUTO;
private RetrieveSettings.PROFILE_MODE profileMode = RetrieveSettings.PROFILE_MODE.OFF;
private RetrieveSettings.TERMINOLOGY_FILTER_MODE terminologyParameterMode =
RetrieveSettings.TERMINOLOGY_FILTER_MODE.AUTO;
public RetrieveSettings.SEARCH_FILTER_MODE getSearchParameterMode() {
return searchParameterMode;
}
public void setSearchParameterMode(RetrieveSettings.SEARCH_FILTER_MODE searchParameterMode) {
this.searchParameterMode = searchParameterMode;
}
public RetrieveSettings.PROFILE_MODE getProfileMode() {
return profileMode;
}
public void setProfileMode(RetrieveSettings.PROFILE_MODE profileMode) {
this.profileMode = profileMode;
}
public RetrieveSettings.TERMINOLOGY_FILTER_MODE getTerminologyParameterMode() {
return terminologyParameterMode;
}
public void setTerminologyParameterMode(RetrieveSettings.TERMINOLOGY_FILTER_MODE terminologyParameterMode) {
this.terminologyParameterMode = terminologyParameterMode;
}
public RetrieveSettings getRetrieveSettings() {
var retrieveSettings = new RetrieveSettings();
retrieveSettings.setSearchParameterMode(searchParameterMode);
retrieveSettings.setProfileMode(profileMode);
retrieveSettings.setTerminologyParameterMode(terminologyParameterMode);
return retrieveSettings;
}
}

View File

@@ -2,14 +2,18 @@ package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings;
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql")
public class CqlProperties {
private Boolean use_embedded_libraries = true;
private CqlCompilerProperties compiler = new CqlCompilerProperties();
private CqlRuntimeProperties runtime = new CqlRuntimeProperties();
private TerminologySettings terminology = new TerminologySettings();
private RetrieveSettings data = new RetrieveSettings();
private CqlData data = new CqlData();
public Boolean getUse_embedded_libraries() {
return use_embedded_libraries;
@@ -43,11 +47,15 @@ public class CqlProperties {
this.terminology = terminology;
}
public RetrieveSettings getData() {
public CqlData getData() {
return data;
}
public void setData(RetrieveSettings data) {
public void setData(CqlData data) {
this.data = data;
}
public RetrieveSettings getRetrieveSettings() {
return data.getRetrieveSettings();
}
}

View File

@@ -1,5 +1,10 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.runtime")
public class CqlRuntimeProperties {
private Boolean debug_logging_enabled = false;

View File

@@ -0,0 +1,43 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "hapi.fhir.cr.cql.terminology")
@Configuration
public class CqlTerminologyProperties {
private TerminologySettings.VALUESET_EXPANSION_MODE valuesetExpansionMode =
TerminologySettings.VALUESET_EXPANSION_MODE.AUTO;
private TerminologySettings.VALUESET_MEMBERSHIP_MODE valuesetMembershipMode =
TerminologySettings.VALUESET_MEMBERSHIP_MODE.AUTO;
private TerminologySettings.CODE_LOOKUP_MODE codeLookupMode = TerminologySettings.CODE_LOOKUP_MODE.AUTO;
private TerminologySettings.VALUESET_PRE_EXPANSION_MODE valueSetPreExpansionMode =
TerminologySettings.VALUESET_PRE_EXPANSION_MODE.USE_IF_PRESENT;
public void setValuesetExpansionMode(TerminologySettings.VALUESET_EXPANSION_MODE valuesetExpansionMode) {
this.valuesetExpansionMode = valuesetExpansionMode;
}
public void setValuesetMembershipMode(TerminologySettings.VALUESET_MEMBERSHIP_MODE valuesetMembershipMode) {
this.valuesetMembershipMode = valuesetMembershipMode;
}
public void setCodeLookupMode(TerminologySettings.CODE_LOOKUP_MODE codeLookupMode) {
this.codeLookupMode = codeLookupMode;
}
public void setValueSetPreExpansionMode(TerminologySettings.VALUESET_PRE_EXPANSION_MODE valueSetPreExpansionMode) {
this.valueSetPreExpansionMode = valueSetPreExpansionMode;
}
public TerminologySettings getTerminologySettings() {
TerminologySettings settings = new TerminologySettings();
settings.setValuesetExpansionMode(valuesetExpansionMode);
settings.setValuesetMembershipMode(valuesetMembershipMode);
settings.setCodeLookupMode(codeLookupMode);
settings.setValuesetPreExpansionMode(valueSetPreExpansionMode);
return settings;
}
}

View File

@@ -23,7 +23,6 @@ import org.opencds.cqf.fhir.cr.measure.CareGapsProperties;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.utility.ValidationProfile;
import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@@ -43,19 +42,13 @@ import java.util.concurrent.Executors;
public class CrCommonConfig {
@Bean
@ConfigurationProperties(prefix = "hapi.fhir.cr")
CrProperties crProperties() {
return new CrProperties();
RetrieveSettings retrieveSettings(CqlData cqlData) {
return cqlData.getRetrieveSettings();
}
@Bean
RetrieveSettings retrieveSettings(CrProperties theCrProperties) {
return theCrProperties.getCql().getData();
}
@Bean
TerminologySettings terminologySettings(CrProperties theCrProperties) {
return theCrProperties.getCql().getTerminology();
TerminologySettings terminologySettings(CqlTerminologyProperties theCqlTerminologyProperties) {
return theCqlTerminologyProperties.getTerminologySettings();
}
@Bean
@@ -65,7 +58,8 @@ public class CrCommonConfig {
@Bean
public EvaluationSettings evaluationSettings(
CrProperties theCrProperties,
CqlRuntimeProperties cqlRuntimeProperties,
CqlCompilerProperties cqlCompilerProperties,
RetrieveSettings theRetrieveSettings,
TerminologySettings theTerminologySettings,
Map<VersionedIdentifier, CompiledLibrary> theGlobalLibraryCache,
@@ -76,7 +70,7 @@ public class CrCommonConfig {
var cqlEngineOptions = cqlOptions.getCqlEngineOptions();
Set<CqlEngine.Options> options = EnumSet.noneOf(CqlEngine.Options.class);
var cqlRuntimeProperties = theCrProperties.getCql().getRuntime();
if (cqlRuntimeProperties.isEnableExpressionCaching()) {
options.add(CqlEngine.Options.EnableExpressionCaching);
}
@@ -91,8 +85,6 @@ public class CrCommonConfig {
var cqlCompilerOptions = new CqlCompilerOptions();
var cqlCompilerProperties = theCrProperties.getCql().getCompiler();
if (cqlCompilerProperties.isEnableDateRangeOptimization()) {
cqlCompilerOptions.setOptions(CqlCompilerOptions.Options.EnableDateRangeOptimization);
}
@@ -159,8 +151,8 @@ public class CrCommonConfig {
return executor;
}
@Bean
CareGapsProperties careGapsProperties(CrProperties theCrProperties) {
@Bean(name = "measure.CareGapsProperties")
org.opencds.cqf.fhir.cr.measure.CareGapsProperties careGapsProperties(CrProperties theCrProperties) {
var careGapsProperties = new CareGapsProperties();
// This check for the resource type really should be happening down in CR where the setting is actually used but
// that will have to wait for a future CR release

View File

@@ -1,13 +1,16 @@
package ca.uhn.fhir.jpa.starter.cr;
import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hapi.fhir.cr")
public class CrProperties {
private Boolean enabled;
private CareGapsProperties careGaps = new CareGapsProperties();
private CqlProperties cql = new CqlProperties();
private TerminologyServerClientSettings terminologyServerClientSettings = new TerminologyServerClientSettings();
public Boolean getEnabled() {

View File

@@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
@@ -66,7 +67,7 @@ public class McpServerConfig {
return HttpServletStreamableServerTransportProvider.builder()
.disallowDelete(false)
.mcpEndpoint(properties.getMcpEndpoint())
.objectMapper(new ObjectMapper())
.jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper()))
// .contextExtractor((serverRequest, context) -> context)
.build();
}