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:
Jens Kristian Villadsen
2025-09-17 06:51:19 +02:00
committed by GitHub
parent 5585170c7d
commit 680255ff62
18 changed files with 1080 additions and 2 deletions

View 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());
}
}
}