Skip to content

Commit fe2c41f

Browse files
authored
Add validation for prompt name uniqueness across the MCP Server as well as in a single smithy model at build time. (#833)
1 parent 68ac4e6 commit fe2c41f

File tree

13 files changed

+721
-7
lines changed

13 files changed

+721
-7
lines changed

examples/mcp-server/src/main/resources/software/amazon/smithy/java/example/server/mcp/main.smithy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use smithy.ai#prompts
1919
arguments: GetCodingStatisticsInput
2020
preferWhen: "User wants to analyze developer productivity, review coding activity, or understand technology usage patterns"
2121
}
22-
employee_lookup: {
22+
Test_employee: {
2323
description: "General employee lookup and information retrieval service"
2424
template: "This service provides employee information including personal details and coding statistics. Use get_employee_info for basic details or get_coding_stats for development metrics."
2525
preferWhen: "User needs any employee-related information or wants to understand available employee data"

mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpServer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package software.amazon.smithy.java.mcp.server;
77

8+
import static software.amazon.smithy.java.mcp.server.PromptLoader.normalize;
9+
810
import java.io.InputStream;
911
import java.io.OutputStream;
1012
import java.io.PrintWriter;
@@ -146,7 +148,7 @@ private void handleRequest(JsonRpcRequest req) {
146148
var promptName = req.getParams().getMember("name").asString();
147149
var promptArguments = req.getParams().getMember("arguments");
148150

149-
var prompt = prompts.get(promptName);
151+
var prompt = prompts.get(normalize(promptName));
150152

151153
if (prompt == null) {
152154
internalError(req, new RuntimeException("Prompt not found: " + promptName));

mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/PromptLoader.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public static Map<String, Prompt> loadPrompts(Collection<Service> services) {
5454

5555
});
5656
for (Map.Entry<String, PromptTemplateDefinition> entry : promptDefinitions.entrySet()) {
57-
var promptName = entry.getKey().toLowerCase();
57+
var promptName = entry.getKey();
5858
var promptTemplateDefinition = entry.getValue();
5959
var templateString = promptTemplateDefinition.getTemplate();
6060

@@ -76,8 +76,20 @@ public static Map<String, Prompt> loadPrompts(Collection<Service> services) {
7676
: List.of())
7777
.build();
7878

79+
var normalizedName = normalize(promptName);
80+
81+
if (prompts.containsKey(normalizedName)) {
82+
var existingPrompt = prompts.get(normalizedName);
83+
throw new RuntimeException(String.format(
84+
"Duplicate normalized prompt name '%s' found. " +
85+
"Original name '%s' conflicts with previously registered name '%s'.",
86+
normalizedName,
87+
promptName,
88+
existingPrompt.promptInfo().getName()));
89+
}
90+
7991
prompts.put(
80-
promptName,
92+
normalizedName,
8193
new Prompt(promptInfo, finalTemplateString));
8294
}
8395
}
@@ -118,4 +130,8 @@ public static List<PromptArgument> convertArgumentShapeToPromptArgument(Schema a
118130

119131
return promptArguments;
120132
}
133+
134+
public static String normalize(String promptName) {
135+
return promptName.toLowerCase();
136+
}
121137
}

mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,16 @@ void testPromptsList() {
395395
assertEquals("search_users", servicePromptMap.get("name").asString());
396396
assertEquals("Test Template", servicePromptMap.get("description").asString());
397397
assertTrue(servicePromptMap.get("arguments").asList().isEmpty());
398+
399+
var promptNames = prompts.stream()
400+
.map(p -> p.asStringMap().get("name").asString())
401+
.toList();
402+
assertTrue(promptNames.contains("search_users"));
403+
assertTrue(promptNames.contains("perform_operation"));
404+
405+
for (String name : promptNames) {
406+
assertEquals(name.toLowerCase(), name, "Prompt name should be normalized to lowercase: " + name);
407+
}
398408
}
399409

400410
@Test
@@ -430,6 +440,76 @@ void testPromptsGetWithValidPrompt() {
430440
assertEquals("Search for if many results expected.", content.get("text").asString());
431441
}
432442

443+
@Test
444+
void testPromptsGetWithDifferentCasing() {
445+
server = McpServer.builder()
446+
.input(input)
447+
.output(output)
448+
.addService("test-mcp",
449+
ProxyService.builder()
450+
.service(ShapeId.from("smithy.test#TestService"))
451+
.proxyEndpoint("http://localhost")
452+
.model(MODEL)
453+
.build())
454+
.build();
455+
456+
server.start();
457+
458+
// Test with uppercase prompt name
459+
write("prompts/get",
460+
Document.of(Map.of(
461+
"name",
462+
Document.of("SEARCH_USERS"))));
463+
var response = read();
464+
var result = response.getResult().asStringMap();
465+
466+
assertEquals("Test Template", result.get("description").asString());
467+
var messages = result.get("messages").asList();
468+
assertEquals(1, messages.size());
469+
470+
var message = messages.get(0).asStringMap();
471+
assertEquals("user", message.get("role").asString());
472+
var content = message.get("content").asStringMap();
473+
assertEquals("text", content.get("type").asString());
474+
assertEquals("Search for if many results expected.", content.get("text").asString());
475+
476+
// Test with mixed case prompt name
477+
write("prompts/get",
478+
Document.of(Map.of(
479+
"name",
480+
Document.of("Search_Users"))));
481+
response = read();
482+
result = response.getResult().asStringMap();
483+
484+
assertEquals("Test Template", result.get("description").asString());
485+
messages = result.get("messages").asList();
486+
assertEquals(1, messages.size());
487+
488+
message = messages.get(0).asStringMap();
489+
assertEquals("user", message.get("role").asString());
490+
content = message.get("content").asStringMap();
491+
assertEquals("text", content.get("type").asString());
492+
assertEquals("Search for if many results expected.", content.get("text").asString());
493+
494+
// Test with perform_operation prompt in different case
495+
write("prompts/get",
496+
Document.of(Map.of(
497+
"name",
498+
Document.of("PERFORM_OPERATION"))));
499+
response = read();
500+
result = response.getResult().asStringMap();
501+
502+
assertEquals("perform operation", result.get("description").asString());
503+
messages = result.get("messages").asList();
504+
assertEquals(1, messages.size());
505+
506+
message = messages.get(0).asStringMap();
507+
assertEquals("user", message.get("role").asString());
508+
content = message.get("content").asStringMap();
509+
assertEquals("text", content.get("type").asString());
510+
assertEquals("use tool TestOperation with some information.", content.get("text").asString());
511+
}
512+
433513
@Test
434514
void testPromptsGetWithInvalidPrompt() {
435515
server = McpServer.builder()
@@ -452,6 +532,15 @@ void testPromptsGetWithInvalidPrompt() {
452532
var response = read();
453533
assertNotNull(response.getError());
454534
assertTrue(response.getError().getMessage().contains("Prompt not found: nonexistent_prompt"));
535+
536+
// Test with invalid prompt in different case - should still fail
537+
write("prompts/get",
538+
Document.of(Map.of(
539+
"name",
540+
Document.of("NONEXISTENT_PROMPT"))));
541+
response = read();
542+
assertNotNull(response.getError());
543+
assertTrue(response.getError().getMessage().contains("Prompt not found: NONEXISTENT_PROMPT"));
455544
}
456545

457546
@Test

0 commit comments

Comments
 (0)