fix: improve URI template matching to properly escape special characters (#599)

- Enhanced DefaultMcpUriTemplateManager to use Pattern.quote() for escaping special regex characters like '?'
- Replaced simple string replacement regex generation with robust pattern building
- Added test case to verify URI matching with query parameters (e.g., "file://name/search?={search}")
- Fixed typo: renamed DeafaultMcpUriTemplateManagerFactory to DefaultMcpUriTemplateManagerFactory
- Updated all references across server classes and tests

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Sheegan Sri G M 2025-10-04 09:42:58 +05:30 • committed by Christian Tzolov
commit 19a8c00a47
6 changed files with 43 additions and 16 deletions

View file

@ -36,7 +36,7 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
@ -120,7 +120,7 @@ public class McpAsyncServer {
private List<String> protocolVersions;
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
/**
* Create a new McpAsyncServer with the given transport provider and capabilities.

View file

@ -25,7 +25,7 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import reactor.core.publisher.Mono;
@ -268,7 +268,7 @@ public interface McpServer {
*/
abstract class AsyncSpecification<S extends AsyncSpecification<S>> {
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
McpJsonMapper jsonMapper;
@ -865,7 +865,7 @@ public interface McpServer {
*/
abstract class SyncSpecification<S extends SyncSpecification<S>> {
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
McpJsonMapper jsonMapper;
@ -1407,7 +1407,7 @@ public interface McpServer {
private final McpStatelessServerTransport transport;
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
McpJsonMapper jsonMapper;
@ -1870,7 +1870,7 @@ public interface McpServer {
boolean immediateExecution = false;
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
McpJsonMapper jsonMapper;

View file

@ -20,7 +20,7 @@ import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
@ -74,7 +74,7 @@ public class McpStatelessAsyncServer {
private List<String> protocolVersions;
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
private final JsonSchemaValidator jsonSchemaValidator;

View file

@ -141,12 +141,30 @@ public class DefaultMcpUriTemplateManager implements McpUriTemplateManager {
return uri.equals(this.uriTemplate);
}
// Convert the pattern to a regex
String regex = this.uriTemplate.replaceAll("\\{[^/]+?\\}", "([^/]+?)");
regex = regex.replace("/", "\\/");
// Convert the URI template into a robust regex pattern that escapes special
// characters like '?'.
StringBuilder patternBuilder = new StringBuilder("^");
Matcher variableMatcher = URI_VARIABLE_PATTERN.matcher(this.uriTemplate);
int lastEnd = 0;
while (variableMatcher.find()) {
// Append the literal part of the template, safely quoted
String textBefore = this.uriTemplate.substring(lastEnd, variableMatcher.start());
patternBuilder.append(Pattern.quote(textBefore));
// Append a capturing group for the variable itself
patternBuilder.append("([^/]+?)");
lastEnd = variableMatcher.end();
}
// Append any remaining literal text after the last variable
if (lastEnd < this.uriTemplate.length()) {
patternBuilder.append(Pattern.quote(this.uriTemplate.substring(lastEnd)));
}
patternBuilder.append("$");
// Check if the URI matches the regex
return Pattern.compile(regex).matcher(uri).matches();
return Pattern.compile(patternBuilder.toString()).matcher(uri).matches();
}
@Override

View file

@ -0,0 +1,24 @@
/*
* Copyright 2025 - 2025 the original author or authors.
*/
package io.modelcontextprotocol.util;
/**
* @author Christian Tzolov
*/
public class DefaultMcpUriTemplateManagerFactory implements McpUriTemplateManagerFactory {
/**
* Creates a new instance of {@link McpUriTemplateManager} with the specified URI
* template.
* @param uriTemplate The URI template to be used for variable extraction
* @return A new instance of {@link McpUriTemplateManager}
* @throws IllegalArgumentException if the URI template is null or empty
*/
@Override
public McpUriTemplateManager create(String uriTemplate) {
return new DefaultMcpUriTemplateManager(uriTemplate);
}
}

View file

@ -12,7 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManager;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import org.junit.jupiter.api.BeforeEach;
@ -29,7 +29,7 @@ public class McpUriTemplateManagerTests {
@BeforeEach
void setUp() {
this.uriTemplateFactory = new DeafaultMcpUriTemplateManagerFactory();
this.uriTemplateFactory = new DefaultMcpUriTemplateManagerFactory();
}
@Test
@ -94,4 +94,13 @@ public class McpUriTemplateManagerTests {
assertFalse(uriTemplateManager.matches("/api/users/123/comments/456"));
}
@Test
void shouldMatchUriWithQueryParameters() {
String templateWithQuery = "file://name/search?={search}";
var uriTemplateManager = this.uriTemplateFactory.create(templateWithQuery);
assertTrue(uriTemplateManager.matches("file://name/search?=abcd"),
"Should correctly match a URI containing query parameters.");
}
}