Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/angular_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
# https://github.com/actions/setup-node
node-version: 20
node-version: 24
cache: npm
cache-dependency-path: matchbox-frontend/package-lock.json

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
2025/10/21 Release 4.0.14

- Further MCP Server integration [#398](https://github.com/ahdis/matchbox/issues/398)

2025/10/04 Release 4.0.13

- MCP Server integration (#398)
Expand Down
18 changes: 16 additions & 2 deletions docs/validation-with-ai-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ spring:
enabled: true
```

The MCP-Server can be reached at the endpoint `http://<<your-url>>/matchbox/mcp/sse`
The MCP-Server can be reached at the endpoint `http://<<your-url>>/matchbox(v3)/mcp/message`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Inconsistent use of parentheses in endpoint URL.

Ensure the endpoint format is consistent across all documentation references.

Suggested implementation:

The MCP-Server can be reached at the endpoint `http://<<your-url>>/matchbox/v3/mcp/message`

Review the rest of the documentation for any other references to the endpoint and update them to use /matchbox/v3/mcp/message (without parentheses) for consistency.


### Setting up matchbox for Claude Desktop

Expand All @@ -98,6 +98,20 @@ For Free users connecting to remote servers is currently not officially supporte
Example Claude Desktop:
![Claude Desktop MCP example](assets/claude_desktop_mcp_example.png)

```json
{
"mcpServers": {
"matchbox": {
"command": "npx",
"args": [
"mcp-remote@latest",
"http://localhost:8080/matchboxv3/mcp/message"
]
}
}
}
```

### Setting up matchbox for VS Code GitHub Copilot

First, make sure the `chat.mcp.enabled` setting in VS Code is enabled.
Expand All @@ -112,7 +126,7 @@ Add matchbox as MCP-Server as such:
"mcp": {
"servers": {
"matchbox": {
"url": "http://<<your-url>>/matchbox/mcp/sse"
"url": "http://<<your-url>>/matchboxv3/mcp/message"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion matchbox-engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<artifactId>matchbox</artifactId>
<groupId>health.matchbox</groupId>
<version>4.0.13</version>
<version>4.0.14</version>
</parent>

<artifactId>matchbox-engine</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,8 @@ public List<ValidationMessage> validate(final @NonNull FhirFormat format,
final List<ValidationMessage> messages = new ArrayList<>();
final InstanceValidator validator = getValidator(format);
validator.validate(null, messages, stream, format, (sd != null) ? new ArrayList<>(List.of(sd)) : new ArrayList<>());
log.info("finished validation " + sd.getUrl() + "|" + sd.getVersion() + " "
+ (sd.getDateElement() != null ? "(" + sd.getDateElement().asStringValue() + ")" : ""));
return this.filterValidationMessages(messages);
}

Expand Down
4 changes: 2 additions & 2 deletions matchbox-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion matchbox-frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "matchbox",
"version": "3.9.13",
"version": "3.9.14",
"license": "MIT",
"scripts": {
"ng": "ng",
Expand Down
2 changes: 1 addition & 1 deletion matchbox-frontend/src/app/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</mat-card-content>
<mat-card-footer>
<p>
matchbox version: {{ version }} | <a href="https://www.ahdis.ch" rel="external nofollow noopener" target="_blank">
Matchbox version: {{ version }} | <a href="https://www.ahdis.ch" rel="external nofollow noopener" target="_blank">
contact</a>
</p>
</mat-card-footer>
Expand Down
2 changes: 1 addition & 1 deletion matchbox-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>matchbox</artifactId>
<groupId>health.matchbox</groupId>
<version>4.0.13</version>
<version>4.0.14</version>
</parent>

<artifactId>matchbox-server</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ public MockHttpServletRequest buildRequest() {
if (profile != null && !profile.isBlank()) {
req.addParameter("profile", profile);
}
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()));
}
req.addHeader("Content-Type", "text/plain; charset=UTF-8");
}
default -> throw new IllegalArgumentException("Unsupported interaction: " + interaction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import ca.uhn.fhir.context.FhirContext;
import ch.ahdis.matchbox.mcp.ToolFactory;
import ca.uhn.fhir.jpa.starter.AppProperties;
import ch.ahdis.matchbox.util.CrossVersionResourceUtils;
import ch.ahdis.matchbox.util.http.MatchboxFhirFormat;
import ca.uhn.fhir.jpa.starter.mcp.CallToolResultFactory;
import ca.uhn.fhir.jpa.starter.mcp.Interaction;
import ca.uhn.fhir.jpa.starter.mcp.RequestBuilder;
Expand All @@ -11,12 +12,16 @@
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;

import org.hl7.fhir.r5.model.OperationDefinition.OperationDefinitionParameterComponent;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.Enumerations.OperationParameterUse;
import org.hl7.fhir.r5.model.OperationDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Component;

import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -46,6 +51,10 @@ public List<McpServerFeatures.SyncToolSpecification> generateTools() {
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.listFhirProfilesToValidateFor())
.callHandler((exchange, request) -> getFhirProfilesToValidateFor(request, Interaction.READ))
.build(),
new McpServerFeatures.SyncToolSpecification.Builder()
.tool(ToolFactory.listValidationParameters())
.callHandler((exchange, request) -> getExtraValidationParameters(request, Interaction.READ))
.build()
);
} catch (JsonProcessingException e) {
Expand Down Expand Up @@ -97,6 +106,52 @@ private McpSchema.CallToolResult getFhirProfilesToValidateFor(McpSchema.CallTool
}
}

private McpSchema.CallToolResult getExtraValidationParameters(McpSchema.CallToolRequest contextMap, Interaction interaction) {
var response = new MockHttpServletResponse();
contextMap.arguments().put("resourceType", "OperationDefinition");
contextMap.arguments().put("id", "-s-validate");
var request = new RequestBuilder(restfulServer, 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);
}

FhirContext fhirR5Context = FhirContext.forR5Cached();
OperationDefinition operationDefinition = fhirR5Context.newJsonParser().parseResource(OperationDefinition.class, body);
List<OperationDefinitionParameterComponent> parameters = operationDefinition.getParameter().stream()
.filter(p -> OperationParameterUse.IN.equals(p.getUse()))
.filter(p -> !"resource".equals(p.getName()))
.filter(p -> !"profile".equals(p.getName()))
.map(p -> {
p.setUse(null);
return p;
}).toList();


StringWriter result = new StringWriter();
result.append("[ ");
for (int i = 0; i < parameters.size(); i++) {
CrossVersionResourceUtils.serializeR5(parameters.get(i), MatchboxFhirFormat.JSON, result);
result.append(",");
}
result.append("]");

String json = result.toString();
return CallToolResultFactory.successFhirBody(json);
} 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());
}
}

private McpSchema.CallToolResult getFhirImplementationGuides(McpSchema.CallToolRequest contextMap, Interaction interaction) {
var response = new MockHttpServletResponse();
contextMap.arguments().put("resourceType", "ImplementationGuide");
Expand Down Expand Up @@ -135,12 +190,25 @@ private McpSchema.CallToolResult getFhirImplementationGuides(McpSchema.CallToolR
private McpSchema.CallToolResult getValidationResult(McpSchema.CallToolRequest contextMap, Interaction interaction) {

var response = new MockHttpServletResponse();
var request = new RequestBuilder(restfulServer, contextMap.arguments(), interaction).buildRequest();
if (contextMap.arguments().containsKey("validationparams")) {
Map<String, Object> map = new java.util.HashMap<>();
String validationParams = (String) contextMap.arguments().get("validationparams");
// Parse the validationParams string into a Map
String[] params = validationParams.split(",");
for (String param : params) {
String[] keyValue = param.split("=");
if (keyValue.length == 2) {
map.put(keyValue[0].trim(), keyValue[1].trim());
}
}
contextMap.arguments().put("query", map);
} else {
Map<String, Object> map = new java.util.HashMap<>();
map.put("analyzeOutcomeWithAI", "false");
contextMap.arguments().put("query", map);
}

Map<String, Object> map = new java.util.HashMap<>();
// we do not want to use AI for validation, this can do mcp client itself
map.put("analyzeOutcomeWithAI", "false");
contextMap.arguments().put("query", map);
var request = new RequestBuilder(restfulServer, contextMap.arguments(), interaction).buildRequest();

try {
restfulServer.handleRequest(interaction.asRequestType(), request, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public class ToolFactory {
"profile": {
"type": "string",
"description": "The FHIR profile to validate against"
},
"validationparams": {
"type": "string",
"description": "Additional validation parameters separated by \\",\\". For example: \\"txServer=http://tx.fhir.org,txUseEcosystem=false\\"."
}
},
"required": ["resource", "profile"]
Expand All @@ -35,9 +39,9 @@ public class ToolFactory {
"includeVersions": {
"type": "boolean",
"description": "include older versions of the installed FHIR Implementation Guides, defaults to false"
}
}
}
}
""";

private static final String LIST_PROFILES_SCHEMA =
Expand All @@ -57,6 +61,15 @@ public class ToolFactory {
}
""";

private static final String LIST_VALIDATIONPARAMETERS_SCHEMA =
"""
{
"type": "object",
"properties": {
}
}
""";

private static final String LIST_FHIR_IGS_OUTPUT_SCHEMA =
"""
{
Expand Down Expand Up @@ -123,6 +136,16 @@ public static Tool listFhirProfilesToValidateFor() throws JsonProcessingExceptio
.build();
}

public static Tool listValidationParameters() throws JsonProcessingException {
return new Tool.Builder()
.name("list-validation-parameters")
.description("List additional available parameters for validation")
.inputSchema(mapper.readValue(LIST_VALIDATIONPARAMETERS_SCHEMA, McpSchema.JsonSchema.class))
// .outputSchema(LIST_FHIR_IGS_OUTPUT_SCHEMA)
.build();
}


public static final ObjectMapper mapper = new ObjectMapper()
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_50;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.instance.model.api.IBase;

import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -67,6 +68,14 @@ public static void serializeR5(final Resource resource,
parser.encodeResourceToWriter(resource, writer);
}

public static void serializeR5(final IBase theElement,
final MatchboxFhirFormat format,
final Writer writer) throws IOException {
final var parser = getHapiParser(format, FhirVersionEnum.R5);
parser.setPrettyPrint(true);
writer.append(parser.encodeToString(theElement));
}

public static org.hl7.fhir.r4.model.Resource convertResource(final org.hl7.fhir.r4b.model.Resource resource) {
// Is there a better way of doing this?
return VersionConvertorFactory_40_50.convertResource(VersionConvertorFactory_43_50.convertResource(resource));
Expand Down
Loading