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
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