Feature/mcp (#846)
* 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 * Revert "Fixed some wirings" This reverts commit c9d3bc0b3b6756d7b15f5d2cf6100c99784fb868. * Revert "Fixed CDS hooks configuration" This reverts commit 67c4279100bf14432c164906235ea6348ee8af22. --------- 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:
committed by
GitHub
parent
5585170c7d
commit
680255ff62
@@ -21,9 +21,9 @@ import java.util.Set;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
|
||||
@EnableConfigurationProperties
|
||||
@ConfigurationProperties(prefix = "hapi.fhir")
|
||||
@Configuration
|
||||
@EnableConfigurationProperties
|
||||
public class AppProperties {
|
||||
|
||||
private final Set<String> auto_version_reference_at_paths = new HashSet<>();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package ca.uhn.fhir.jpa.starter.mcp;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class CallToolResultFactory {
|
||||
|
||||
public static McpSchema.CallToolResult success(
|
||||
String resourceType, Interaction interaction, String response, int status) {
|
||||
Map<String, Object> payload = Map.of(
|
||||
"resourceType", resourceType,
|
||||
"interaction", interaction,
|
||||
"response", response,
|
||||
"status", status);
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String jacksonData;
|
||||
try {
|
||||
jacksonData = objectMapper.writeValueAsString(payload);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addContent(new McpSchema.TextContent(jacksonData))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpSchema.CallToolResult failure(String message) {
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.isError(true)
|
||||
.addTextContent(message)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
34
src/main/java/ca/uhn/fhir/jpa/starter/mcp/Interaction.java
Normal file
34
src/main/java/ca/uhn/fhir/jpa/starter/mcp/Interaction.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package ca.uhn.fhir.jpa.starter.mcp;
|
||||
|
||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||
|
||||
public enum Interaction {
|
||||
CALL_CDS_HOOK("call-cds-hook"),
|
||||
SEARCH("search"),
|
||||
READ("read"),
|
||||
CREATE("create"),
|
||||
UPDATE("update"),
|
||||
DELETE("delete"),
|
||||
PATCH("patch"),
|
||||
TRANSACTION("transaction");
|
||||
|
||||
private final String name;
|
||||
|
||||
Interaction(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public RequestTypeEnum asRequestType() {
|
||||
return switch (this) {
|
||||
case SEARCH, READ -> RequestTypeEnum.GET;
|
||||
case CREATE, TRANSACTION, CALL_CDS_HOOK -> RequestTypeEnum.POST;
|
||||
case UPDATE -> RequestTypeEnum.PUT;
|
||||
case DELETE -> RequestTypeEnum.DELETE;
|
||||
case PATCH -> RequestTypeEnum.PATCH;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package ca.uhn.fhir.jpa.starter.mcp;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.server.McpBridge;
|
||||
import ca.uhn.fhir.rest.server.McpCdsBridge;
|
||||
import ca.uhn.fhir.rest.server.McpFhirBridge;
|
||||
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.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// https://mcp-cn.ssshooter.com/sdk/java/mcp-server#sse-servlet
|
||||
// https://www.baeldung.com/spring-ai-model-context-protocol-mcp
|
||||
// https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java
|
||||
// https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server
|
||||
// https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java
|
||||
// https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html
|
||||
@Configuration
|
||||
@ConditionalOnProperty(
|
||||
prefix = "spring.ai.mcp.server",
|
||||
name = {"enabled"},
|
||||
havingValue = "true")
|
||||
public class McpServerConfig {
|
||||
|
||||
private static final String SSE_ENDPOINT = "/sse";
|
||||
private static final String SSE_MESSAGE_ENDPOINT = "/mcp/message";
|
||||
|
||||
@Bean
|
||||
public McpSyncServer syncServer(
|
||||
List<McpBridge> mcpBridges, McpStreamableServerTransportProvider transportProvider) {
|
||||
return McpServer.sync(transportProvider)
|
||||
.tools(mcpBridges.stream()
|
||||
.flatMap(bridge -> bridge.generateTools().stream())
|
||||
.toList())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpFhirBridge mcpFhirBridge(RestfulServer restfulServer) {
|
||||
return new McpFhirBridge(restfulServer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
prefix = "hapi.fhir.cr",
|
||||
name = {"enabled"},
|
||||
havingValue = "true")
|
||||
public McpCdsBridge mcpCdsBridge(FhirContext fhirContext, ICdsServiceRegistry cdsServiceRegistry) {
|
||||
|
||||
return new McpCdsBridge(
|
||||
fhirContext, cdsServiceRegistry, new CdsHooksObjectMapperFactory(fhirContext).newMapper());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpServletStreamableServerTransportProvider servletSseServerTransportProvider(
|
||||
/*McpServerProperties properties*/ ) {
|
||||
|
||||
return HttpServletStreamableServerTransportProvider.builder()
|
||||
.disallowDelete(false)
|
||||
.mcpEndpoint(SSE_MESSAGE_ENDPOINT)
|
||||
.objectMapper(new ObjectMapper())
|
||||
// .contextExtractor((serverRequest, context) -> context)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletRegistrationBean customServletBean(
|
||||
HttpServletStreamableServerTransportProvider transportProvider /*, McpServerProperties properties*/) {
|
||||
return new ServletRegistrationBean<>(transportProvider, SSE_MESSAGE_ENDPOINT, SSE_ENDPOINT);
|
||||
}
|
||||
}
|
||||
120
src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java
Normal file
120
src/main/java/ca/uhn/fhir/jpa/starter/mcp/RequestBuilder.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package ca.uhn.fhir.jpa.starter.mcp;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import com.google.gson.Gson;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
public class RequestBuilder {
|
||||
|
||||
private final FhirContext fhirContext;
|
||||
private final String resourceType;
|
||||
private final Interaction interaction;
|
||||
private final Map<String, Object> config;
|
||||
/**
|
||||
* Constructs a RequestBuilder for a specific FHIR interaction.
|
||||
*
|
||||
* @param fhirContext the FHIR context
|
||||
* @param contextMap a map containing configuration parameters, including 'resourceType'
|
||||
* @param interaction the type of interaction (e.g., SEARCH, READ, CREATE, etc.)
|
||||
*/
|
||||
public RequestBuilder(FhirContext fhirContext, Map<String, Object> contextMap, Interaction interaction) {
|
||||
this.config = contextMap;
|
||||
if (interaction == Interaction.TRANSACTION) this.resourceType = "";
|
||||
else if (contextMap.get("resourceType") instanceof String rt && !rt.isBlank()) this.resourceType = rt;
|
||||
else throw new IllegalArgumentException("Missing or invalid 'resourceType' in contextMap");
|
||||
|
||||
this.interaction = interaction;
|
||||
this.fhirContext = fhirContext;
|
||||
}
|
||||
|
||||
public MockHttpServletRequest buildRequest() {
|
||||
String basePath = "/" + resourceType;
|
||||
String method;
|
||||
MockHttpServletRequest req;
|
||||
|
||||
switch (interaction) {
|
||||
case SEARCH -> {
|
||||
method = "GET";
|
||||
req = new MockHttpServletRequest(method, basePath);
|
||||
Map<?, ?> sp = null;
|
||||
if (config.get("query") instanceof Map<?, ?> q) {
|
||||
sp = q;
|
||||
} else if (config.get("searchParams") instanceof Map<?, ?> s) {
|
||||
sp = s;
|
||||
}
|
||||
if (sp != null) {
|
||||
sp.forEach((k, v) -> req.addParameter(k.toString(), v.toString()));
|
||||
}
|
||||
}
|
||||
case READ -> {
|
||||
method = "GET";
|
||||
String id = requireString();
|
||||
req = new MockHttpServletRequest(method, basePath + "/" + id);
|
||||
}
|
||||
case CREATE, TRANSACTION -> {
|
||||
method = "POST";
|
||||
req = new MockHttpServletRequest(method, basePath);
|
||||
applyResourceBody(req);
|
||||
}
|
||||
case UPDATE -> {
|
||||
method = "PUT";
|
||||
String id = requireString();
|
||||
req = new MockHttpServletRequest(method, basePath + "/" + id);
|
||||
applyResourceBody(req);
|
||||
}
|
||||
case DELETE -> {
|
||||
method = "DELETE";
|
||||
String id = requireString();
|
||||
req = new MockHttpServletRequest(method, basePath + "/" + id);
|
||||
}
|
||||
case PATCH -> {
|
||||
method = "PATCH";
|
||||
String id = requireString();
|
||||
req = new MockHttpServletRequest(method, basePath + "/" + id);
|
||||
applyPatchBody(req);
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unsupported interaction: " + interaction);
|
||||
}
|
||||
|
||||
req.setContentType("application/fhir+json");
|
||||
req.addHeader("Accept", "application/fhir+json");
|
||||
return req;
|
||||
}
|
||||
|
||||
private void applyResourceBody(MockHttpServletRequest req) {
|
||||
Object resourceObj = config.get("resource");
|
||||
String json;
|
||||
if (resourceObj instanceof Map<?, ?>) json = new Gson().toJson(resourceObj, Map.class);
|
||||
else if (resourceObj instanceof String) json = resourceObj.toString();
|
||||
else throw new IllegalArgumentException("Unsupported resource body type: " + resourceObj.getClass());
|
||||
req.setContent(json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void applyPatchBody(MockHttpServletRequest req) {
|
||||
Object patchBody = config.get("resource");
|
||||
if (patchBody == null) {
|
||||
throw new IllegalArgumentException("Missing 'resource' for patch interaction");
|
||||
}
|
||||
String content;
|
||||
if (patchBody instanceof String s) {
|
||||
content = s;
|
||||
} else if (patchBody instanceof IBaseResource r) {
|
||||
content = fhirContext.newJsonParser().encodeResourceToString(r);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported patch body type: " + patchBody.getClass());
|
||||
}
|
||||
req.setContent(content.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String requireString() {
|
||||
Object val = config.get("id");
|
||||
if (!(val instanceof String s) || s.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing or invalid '" + "id" + "'");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
337
src/main/java/ca/uhn/fhir/jpa/starter/mcp/ToolFactory.java
Normal file
337
src/main/java/ca/uhn/fhir/jpa/starter/mcp/ToolFactory.java
Normal file
@@ -0,0 +1,337 @@
|
||||
package ca.uhn.fhir.jpa.starter.mcp;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||
|
||||
public class ToolFactory {
|
||||
|
||||
private static final String READ_FHIR_RESOURCE_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "type of the resource to read"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "id of the resource to read"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CREATE_FHIR_RESOURCE_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of the resource to create"
|
||||
},
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"description": "Resource content in JSON format"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Headers for create request.\\nAvailable headers: If-None-Exist header for conditional create where the value is search param string.\\nFor example: {\\"If-None-Exist\\": \\"active=false\\"}"
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "resource"]
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String UPDATE_FHIR_RESOURCE_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of the resource to update"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the resource to update"
|
||||
},
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"description": "Updated resource content in JSON format"
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "id", "resource"]
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CONDITIONAL_UPDATE_FHIR_RESOURCE_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of the resource to update"
|
||||
},
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"description": "Updated resource content in JSON format"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Query string with search params separate by \\",\\". For example: \\"_id=pt-1,name=ivan\\". Uses for conditional update."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Headers for create request.\\nAvailable headers: If-None-Match header for conditional update where the value is ETag.\\nFor example: {\\"If-None-Match\\": \\"12345\\"}"
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "resource"]
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CONDITIONAL_PATCH_FHIR_RESOURCE_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of the resource to patch"
|
||||
},
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"description": "Resource content to patch in JSON format"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Query string with search params separate by \\",\\". For example: \\"_id=pt-1,name=ivan\\". Uses for conditional patch."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Headers for create request.\\nAvailable headers: If-None-Match header for conditional patch where the value is ETag.\\nFor example: {\\"If-None-Match\\": \\"12345\\"}"
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "resource"]
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String PATCH_FHIR_RESOURCE_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of the resource to patch"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the resource to patch"
|
||||
},
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"description": "Resource content to patch in JSON format"
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "id", "resource"]
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String DELETE_FHIR_RESOURCE_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of the resource to delete"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the resource to delete"
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "id"]
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String SEARCH_FHIR_RESOURCES_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of the resource to search"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Query string with search params separate by \\",\\". For example: \\"_id=pt-1,name=ivan\\""
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "query"]
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CREATE_FHIR_TRANSACTION_SCHEMA =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "A Bundle resource type with type 'transaction' containing multiple FHIR resources"
|
||||
},
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"description": "A FHIR Bundle Resource content in JSON format"
|
||||
}
|
||||
},
|
||||
"required": ["resourceType", "resource"]
|
||||
}
|
||||
""";
|
||||
|
||||
// TODO Add a tool for the CDS Hooks discovery endpoint
|
||||
// Alternatively, should each service be a separate tool?
|
||||
|
||||
// TODO Add other fields from https://cds-hooks.hl7.org/STU2/#http-request-1
|
||||
// TODO Context here is for the patient-view hook, https://cds-hooks.hl7.org/hooks/STU1/patient-view.html#context
|
||||
private static final String CALL_CDS_HOOK_SCHEMA_2_0_1 =
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {
|
||||
"type": "string",
|
||||
"description": "The CDS Service to call."
|
||||
},
|
||||
"hook": {
|
||||
"type": "string",
|
||||
"description": "The hook that triggered this CDS Service call."
|
||||
},
|
||||
"hookInstance": {
|
||||
"type": "string",
|
||||
"description": "A universally unique identifier (UUID) for this particular hook call."
|
||||
},
|
||||
"hookContext": {
|
||||
"type": "object",
|
||||
"description": "Hook-specific contextual data that the CDS service will need.",
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string",
|
||||
"description": "The id of the current user. Must be in the format [ResourceType]/[id]."
|
||||
},
|
||||
"patientId": {
|
||||
"type": "string",
|
||||
"description": "The FHIR Patient.id of the current patient in context"
|
||||
},
|
||||
"encounterId": {
|
||||
"type": "string",
|
||||
"description": "The FHIR Encounter.id of the current encounter in context."
|
||||
}
|
||||
}
|
||||
},
|
||||
"prefetch": {
|
||||
"type": "object",
|
||||
"description": "Additional data to prefetch for the CDS service call."
|
||||
}
|
||||
},
|
||||
"required": ["service", "hook", "hookInstance", "hookContext"]
|
||||
}
|
||||
""";
|
||||
|
||||
public static Tool readFhirResource() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("read-fhir-resource")
|
||||
.description("Read an individual FHIR resource")
|
||||
.inputSchema(mapper.readValue(READ_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool createFhirResource() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("create-fhir-resource")
|
||||
.description("Create a new FHIR resource")
|
||||
.inputSchema(mapper.readValue(CREATE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool updateFhirResource() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("update-fhir-resource")
|
||||
.description("Update an existing FHIR resource")
|
||||
.inputSchema(mapper.readValue(UPDATE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool conditionalUpdateFhirResource() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("conditional-update-fhir-resource")
|
||||
.description("Conditional update an existing FHIR resource")
|
||||
.inputSchema(mapper.readValue(CONDITIONAL_UPDATE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool conditionalPatchFhirResource() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("conditional-patch-fhir-resource")
|
||||
.description("Conditional patch an existing FHIR resource")
|
||||
.inputSchema(mapper.readValue(CONDITIONAL_PATCH_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool patchFhirResource() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("patch-fhir-resource")
|
||||
.description("Patch an existing FHIR resource")
|
||||
.inputSchema(mapper.readValue(PATCH_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool deleteFhirResource() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("delete-fhir-resource")
|
||||
.description("Delete an existing FHIR resource")
|
||||
.inputSchema(mapper.readValue(DELETE_FHIR_RESOURCE_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool searchFhirResources() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("search-fhir-resources")
|
||||
.description("Search an existing FHIR resources")
|
||||
.inputSchema(mapper.readValue(SEARCH_FHIR_RESOURCES_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool createFhirTransaction() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("create-fhir-transaction")
|
||||
.description("Create a FHIR transaction")
|
||||
.inputSchema(mapper.readValue(CREATE_FHIR_TRANSACTION_SCHEMA, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Tool callCdsHook() throws JsonProcessingException {
|
||||
return new Tool.Builder()
|
||||
.name("call-cds-hook")
|
||||
.description("Call a CDS Hook")
|
||||
.inputSchema(mapper.readValue(CALL_CDS_HOOK_SCHEMA_2_0_1, McpSchema.JsonSchema.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static final ObjectMapper mapper = new ObjectMapper()
|
||||
.enable(JsonParser.Feature.ALLOW_COMMENTS)
|
||||
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
|
||||
.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
|
||||
.enable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
9
src/main/java/ca/uhn/fhir/rest/server/McpBridge.java
Normal file
9
src/main/java/ca/uhn/fhir/rest/server/McpBridge.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface McpBridge {
|
||||
List<McpServerFeatures.SyncToolSpecification> generateTools();
|
||||
}
|
||||
117
src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java
Normal file
117
src/main/java/ca/uhn/fhir/rest/server/McpCdsBridge.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.starter.cdshooks.CdsHooksRequest;
|
||||
import ca.uhn.fhir.jpa.starter.mcp.Interaction;
|
||||
import ca.uhn.fhir.jpa.starter.mcp.ToolFactory;
|
||||
import ca.uhn.fhir.rest.api.server.cdshooks.CdsServiceRequestContextJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.gson.Gson;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class McpCdsBridge implements McpBridge {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpCdsBridge.class);
|
||||
|
||||
private final ICdsServiceRegistry cdsServiceRegistry;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final FhirContext fhirContext;
|
||||
|
||||
public McpCdsBridge(FhirContext fhirContext, ICdsServiceRegistry cdsServiceRegistry, ObjectMapper objectMapper) {
|
||||
this.fhirContext = fhirContext;
|
||||
this.cdsServiceRegistry = cdsServiceRegistry;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public List<McpServerFeatures.SyncToolSpecification> generateTools() {
|
||||
|
||||
try {
|
||||
return List.of(new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.callCdsHook())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.CALL_CDS_HOOK))
|
||||
.build());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private McpSchema.CallToolResult getToolResult(McpSchema.CallToolRequest contextMap, Interaction interaction) {
|
||||
|
||||
if (interaction != Interaction.CALL_CDS_HOOK)
|
||||
throw new UnsupportedOperationException("Unsupported interaction: " + interaction);
|
||||
|
||||
var cdsInvocation = constructCdsHooksRequest(contextMap);
|
||||
var serviceResponseJson = cdsServiceRegistry.callService(
|
||||
contextMap.arguments().get("service").toString(), cdsInvocation);
|
||||
|
||||
final String content;
|
||||
try {
|
||||
content = objectMapper.writeValueAsString(serviceResponseJson);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addContent(new McpSchema.TextContent(content))
|
||||
.build();
|
||||
}
|
||||
|
||||
private @NotNull CdsHooksRequest constructCdsHooksRequest(McpSchema.CallToolRequest callToolRequest) {
|
||||
|
||||
// TODO Build up CDS Hooks request JSON from contextMap
|
||||
var contextMap = callToolRequest.arguments();
|
||||
var request = new CdsHooksRequest();
|
||||
request.setHook(contextMap.get("hook").toString());
|
||||
request.setHookInstance(contextMap.get("hookInstance").toString());
|
||||
|
||||
// Context
|
||||
var context = new CdsServiceRequestContextJson();
|
||||
Object hookContextObj = contextMap.get("hookContext");
|
||||
if (hookContextObj instanceof Map<?, ?> hookContext) {
|
||||
if (hookContext.containsKey("userId")) {
|
||||
context.put("userId", String.valueOf(hookContext.get("userId")));
|
||||
}
|
||||
if (hookContext.containsKey("patientId")) {
|
||||
context.put("patientId", String.valueOf(hookContext.get("patientId")));
|
||||
}
|
||||
if (hookContext.containsKey("encounterId")) {
|
||||
context.put("encounterId", String.valueOf(hookContext.get("encounterId")));
|
||||
}
|
||||
}
|
||||
request.setContext(context);
|
||||
|
||||
// Prefetch
|
||||
if (contextMap.containsKey("prefetch")) {
|
||||
var prefetch = contextMap.get("prefetch");
|
||||
if (prefetch instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var prefetchMap = (Map<String, Object>) prefetch;
|
||||
for (Map.Entry<String, Object> entry : prefetchMap.entrySet()) {
|
||||
var key = entry.getKey();
|
||||
var value = entry.getValue();
|
||||
|
||||
// Object is a String -> Object map
|
||||
// Use a standard JSON library to convert it
|
||||
var resource = fhirContext.newJsonParser().parseResource(new Gson().toJson(value));
|
||||
request.addPrefetch(key, resource);
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
"Prefetch object is not a Map: {}",
|
||||
prefetch == null ? "null" : prefetch.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
101
src/main/java/ca/uhn/fhir/rest/server/McpFhirBridge.java
Normal file
101
src/main/java/ca/uhn/fhir/rest/server/McpFhirBridge.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.starter.mcp.CallToolResultFactory;
|
||||
import ca.uhn.fhir.jpa.starter.mcp.Interaction;
|
||||
import ca.uhn.fhir.jpa.starter.mcp.RequestBuilder;
|
||||
import ca.uhn.fhir.jpa.starter.mcp.ToolFactory;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class McpFhirBridge implements McpBridge {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpFhirBridge.class);
|
||||
|
||||
private final RestfulServer restfulServer;
|
||||
private final FhirContext fhirContext;
|
||||
|
||||
public McpFhirBridge(RestfulServer restfulServer) {
|
||||
this.restfulServer = restfulServer;
|
||||
this.fhirContext = restfulServer.getFhirContext();
|
||||
}
|
||||
|
||||
public List<McpServerFeatures.SyncToolSpecification> generateTools() {
|
||||
|
||||
try {
|
||||
return List.of(
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.createFhirResource())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.CREATE))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.readFhirResource())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.READ))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.updateFhirResource())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.UPDATE))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.deleteFhirResource())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.DELETE))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.conditionalPatchFhirResource())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.PATCH))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.searchFhirResources())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.SEARCH))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.conditionalUpdateFhirResource())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.UPDATE))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.patchFhirResource())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.PATCH))
|
||||
.build(),
|
||||
new McpServerFeatures.SyncToolSpecification.Builder()
|
||||
.tool(ToolFactory.createFhirTransaction())
|
||||
.callHandler((exchange, request) -> getToolResult(request, Interaction.TRANSACTION))
|
||||
.build());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private McpSchema.CallToolResult getToolResult(McpSchema.CallToolRequest contextMap, Interaction interaction) {
|
||||
|
||||
var response = new MockHttpServletResponse();
|
||||
var request = new RequestBuilder(fhirContext, contextMap.arguments(), interaction).buildRequest();
|
||||
|
||||
try {
|
||||
restfulServer.handleRequest(interaction.asRequestType(), request, response);
|
||||
var status = response.getStatus();
|
||||
var body = response.getContentAsString();
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
if (body.isBlank()) {
|
||||
return CallToolResultFactory.failure("Empty successful response for " + interaction);
|
||||
}
|
||||
|
||||
return CallToolResultFactory.success(
|
||||
contextMap.arguments().get("resourceType").toString(), interaction, body, status);
|
||||
} else {
|
||||
return CallToolResultFactory.failure(String.format("FHIR server error %d: %s", status, body));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
return CallToolResultFactory.failure("Unexpected error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,65 @@ management:
|
||||
export:
|
||||
enabled: true
|
||||
spring:
|
||||
ai:
|
||||
# Run e.g. `npx @modelcontextprotocol/inspector` and connect to http://localhost:8080/mcp/message using Streamable HTTP
|
||||
|
||||
# Add the following to the MCP server settings file in e.g. cursor or claude (Desktop applications) for local debugging:
|
||||
# cursor:
|
||||
# {
|
||||
# "mcpServers": {
|
||||
# "hapi": {
|
||||
# "url": "http://localhost:8080/mcp/message"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# or claude:
|
||||
# {
|
||||
# "mcpServers": {
|
||||
# "hapi": {
|
||||
# "command": "npx",
|
||||
# "args": [
|
||||
# "mcp-remote@latest",
|
||||
# "http://localhost:8080/mcp/message"
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
mcp:
|
||||
server:
|
||||
# Will be enabled once spring-ai-starter-mcp-server is added as dependency
|
||||
# name: FHIR MCP Server
|
||||
# version: 1.0.0
|
||||
# type: SYNC
|
||||
# instructions: "This server provides access to a FHIR RESTful API. You can use it to query FHIR resources, perform operations, and retrieve data in a structured format."
|
||||
# sse-message-endpoint: /mcp/message
|
||||
# capabilities:
|
||||
# tool: true
|
||||
# resource: true
|
||||
# prompt: true
|
||||
# completion: true
|
||||
# stdio: false
|
||||
enabled: true
|
||||
|
||||
#endpoint: /mcp
|
||||
|
||||
#schema:
|
||||
# fhir-enabled: true
|
||||
# fhir:
|
||||
# base-url: http://localhost:8080/fhir
|
||||
|
||||
#query:
|
||||
# prompt:
|
||||
# template: |
|
||||
# You are a FHIR assistant. Translate the following question into a valid FHIR RESTful API query:
|
||||
# "{{query}}"
|
||||
# Use the provided FHIR schema:
|
||||
# {{schema}}
|
||||
#base-url: /api/v1
|
||||
|
||||
main:
|
||||
allow-bean-definition-overriding: false
|
||||
allow-circular-references: true
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
78
src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java
Normal file
78
src/test/java/ca/uhn/fhir/jpa/starter/McpTests.java
Normal file
@@ -0,0 +1,78 @@
|
||||
package ca.uhn.fhir.jpa.starter;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.searchparam.config.NicknameServiceConfig;
|
||||
import ca.uhn.fhir.jpa.starter.mcp.ToolFactory;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.gson.Gson;
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class, NicknameServiceConfig.class, RepositoryConfig.class}, properties = {"spring.datasource.url=jdbc:h2:mem:dbr4", "hapi.fhir.fhir_version=r4", "hibernate.search.enabled=true", "spring.ai.mcp.server.enabled=true",})
|
||||
public class McpTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Test
|
||||
public void mcpTests() throws JsonProcessingException {
|
||||
|
||||
var fhirContext = FhirContext.forR4();
|
||||
|
||||
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port).endpoint("/mcp/message").build();
|
||||
var client = McpClient.sync(transport).requestTimeout(Duration.ofSeconds(10)).capabilities(McpSchema.ClientCapabilities.builder().roots(true) // Enable roots capability
|
||||
.sampling().build()).build();
|
||||
var initializationResult = client.initialize();
|
||||
|
||||
var tools = client.listTools().tools();
|
||||
assertThat(tools).isNotEmpty();
|
||||
|
||||
var searchToolName = ToolFactory.searchFhirResources().name();
|
||||
var createToolName = ToolFactory.createFhirResource().name();
|
||||
|
||||
assertThat(tools.stream().filter(tool -> tool.name().equals(searchToolName)).findFirst().get()).isNotNull();
|
||||
assertThat(tools.stream().filter(tool -> tool.name().equals(createToolName)).findFirst().get()).isNotNull();
|
||||
|
||||
|
||||
var createMcpRequest = new McpSchema.CallToolRequest.Builder().arguments(Map.of("operation", "create", "resourceType", "Patient", "resource", """
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
"id": "example",
|
||||
"identifier": [
|
||||
{
|
||||
"system": "urn:something",
|
||||
"value": "uncleScrooge"
|
||||
}
|
||||
]
|
||||
}""")).name(createToolName).build();
|
||||
assertThat(client.callTool(createMcpRequest).isError()).isFalse();
|
||||
|
||||
var searchMcpRequest = new McpSchema.CallToolRequest.Builder().arguments(Map.of("operation", "search", "resourceType", "Patient", "query", "identifier=urn:something|uncleScrooge")).name(searchToolName).build();
|
||||
|
||||
var searchResult = client.callTool(searchMcpRequest);
|
||||
assertThat(searchResult.isError()).isFalse();
|
||||
assertThat(searchResult.content().size()).isEqualTo(1);
|
||||
|
||||
var content = ((McpSchema.TextContent) searchResult.content().get(0));
|
||||
var embeddedResponseBundle = new Gson().fromJson(content.text(), LinkedHashMap.class).get("response");
|
||||
var responseBundle = fhirContext.newJsonParser().parseResource(Bundle.class, embeddedResponseBundle.toString());
|
||||
var entries = BundleUtil.toListOfEntries(fhirContext, responseBundle);
|
||||
assertThat(entries.size()).isEqualTo(1);
|
||||
|
||||
client.closeGracefully();
|
||||
}
|
||||
}
|
||||
18
src/test/resources/mcp/hello-patient-request.json
Normal file
18
src/test/resources/mcp/hello-patient-request.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"hook": "patient-view",
|
||||
"hookInstance": "8d5a3a2e-6d8b-4f7c-bb2d-2f1b8cf1d7a1",
|
||||
"context": {
|
||||
"userId": "Practitioner/123",
|
||||
"patientId": "123",
|
||||
"encounterId": "456"
|
||||
},
|
||||
"prefetch": {
|
||||
"item1": {
|
||||
"resourceType": "Patient",
|
||||
"gender": "male",
|
||||
"birthDate": "1989-10-23",
|
||||
"id": "123",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/test/resources/mcp/mcp-hookContext-object.json
Normal file
5
src/test/resources/mcp/mcp-hookContext-object.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"userId": "Practitioner/123",
|
||||
"patientId": "123",
|
||||
"encounterId": "456"
|
||||
}
|
||||
9
src/test/resources/mcp/mpc-prefetch-object.json
Normal file
9
src/test/resources/mcp/mpc-prefetch-object.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"item1": {
|
||||
"resourceType": "Patient",
|
||||
"gender": "male",
|
||||
"birthDate": "1989-10-23",
|
||||
"id": "123",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
42
src/test/resources/mcp/plandefinition-hello-patient.xml
Normal file
42
src/test/resources/mcp/plandefinition-hello-patient.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<PlanDefinition xmlns="http://hl7.org/fhir">
|
||||
<id value="HelloPatientPd" />
|
||||
<meta>
|
||||
<profile value="http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-recommendationdefinition" />
|
||||
</meta>
|
||||
<extension url="http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability">
|
||||
<valueCode value="executable" />
|
||||
</extension>
|
||||
<url value="http://example.org/PlanDefinition/HelloPatientPd" />
|
||||
<identifier>
|
||||
<use value="official" />
|
||||
<value value="PlanDefinition_HelloPatientPd" />
|
||||
</identifier>
|
||||
<name value="PlanDefinition_HelloPatientPd" />
|
||||
<title value="PlanDefinition - Hello Patient" />
|
||||
<type>
|
||||
<coding>
|
||||
<system value="http://terminology.hl7.org/CodeSystem/plan-definition-type" />
|
||||
<code value="eca-rule" />
|
||||
<display value="ECA Rule" />
|
||||
</coding>
|
||||
</type>
|
||||
<status value="draft" />
|
||||
<experimental value="true" />
|
||||
<date value="2024-09-28" />
|
||||
<description value="Demo PlanDefinition for Hello Patient" />
|
||||
<action>
|
||||
<title value="Hello, Patient!" />
|
||||
<description value="Please state the nature of the medical emergency." />
|
||||
<trigger>
|
||||
<type value="named-event" />
|
||||
<name value="patient-view" />
|
||||
</trigger>
|
||||
<condition>
|
||||
<kind value="applicability" />
|
||||
<expression>
|
||||
<language value="text/cql" />
|
||||
<expression value="true" />
|
||||
</expression>
|
||||
</condition>
|
||||
</action>
|
||||
</PlanDefinition>
|
||||
Reference in New Issue
Block a user