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
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ class ServerToolsTest : AbstractServerFeaturesTest() {
assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist")
}

@Test
fun `addTool should succeed with non-conforming tool name`() = runTest {
server.addTool("my invalid tool!", "Tool with non-conforming name", ToolSchema()) {
CallToolResult(listOf(TextContent("It works")))
}

val result = server.removeTool("my invalid tool!")
assertTrue(result, "Tool with non-conforming name should have been registered and removable")
}

@Test
fun `removeTool should throw when tools capability is not supported`() = runTest {
var toolListChangedNotificationReceived = false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.modelcontextprotocol.kotlin.sdk.server

import io.github.oshai.kotlinlogging.KotlinLogging
import io.modelcontextprotocol.kotlin.sdk.server.utils.warnIfInvalidToolName
import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
Expand Down Expand Up @@ -303,7 +304,7 @@ public open class Server(
logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" }
"Server does not support tools capability. Enable it in ServerOptions."
}

warnIfInvalidToolName(tool.name)
toolRegistry.add(RegisteredTool(tool, handler))
}

Expand Down Expand Up @@ -357,6 +358,7 @@ public open class Server(
logger.error { "Failed to add tools: Server does not support tools capability" }
"Server does not support tools capability."
}
toolsToAdd.forEach { warnIfInvalidToolName(it.tool.name) }
toolRegistry.addAll(toolsToAdd)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.modelcontextprotocol.kotlin.sdk.server.utils

import io.github.oshai.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger("ToolNameValidation")

private const val MAX_TOOL_NAME_LENGTH = 128
Comment thread
devcrocod marked this conversation as resolved.

private const val SEP_986_URL = "https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986"

/**
* Validates a tool name against the MCP tool naming standard (SEP-986).
* Returns a list of warnings. An empty list means the name is valid.
*/
internal fun validateToolName(name: String): List<String> {
if (name.isEmpty()) return listOf("Tool name cannot be empty")

val invalidChars = name.toSet().filterNot { it.isAsciiAllowed() }
val remainingInvalidChars = invalidChars - setOf(' ', ',')
Comment thread
devcrocod marked this conversation as resolved.

return listOfNotNull(
"Tool name exceeds maximum length of $MAX_TOOL_NAME_LENGTH characters (current: ${name.length})"
.takeIf { name.length > MAX_TOOL_NAME_LENGTH },
Comment on lines +7 to +23
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

SEP-986 specifies tool names must be 1–64 characters, but this validation uses MAX_TOOL_NAME_LENGTH = 128. This will incorrectly treat names of length 65–128 as conforming. Update the max length constant (and related messages) to 64 to match the spec.

Copilot uses AI. Check for mistakes.
"Tool name contains spaces, which may cause parsing issues"
.takeIf { ' ' in invalidChars },
"Tool name contains commas, which may cause parsing issues"
.takeIf { ',' in invalidChars },
"Tool name contains invalid characters: ${remainingInvalidChars.joinToString(", ") { "\"$it\"" }}"
.takeIf { remainingInvalidChars.isNotEmpty() },
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.takeIf { invalidChars.isNotEmpty() },
Comment thread
devcrocod marked this conversation as resolved.
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.takeIf { name.startsWith('-') || name.endsWith('-') },
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.takeIf { name.startsWith('.') || name.endsWith('.') },
)
}

/**
* Validates the tool [name] and logs a warning if it does not conform to SEP-986.
* Does not block registration — tools with non-conforming names are still accepted.
*/
internal fun warnIfInvalidToolName(name: String) {
val warnings = validateToolName(name)
if (warnings.isEmpty()) return

val warningLines = warnings.joinToString("\n") { " - $it" }
logger.warn {
"""
|Tool name validation warning for "$name":
|$warningLines
|Tool registration will proceed, but this may cause compatibility issues.
|Consider updating the tool name to conform to the MCP tool naming standard.
|See SEP-986: $SEP_986_URL
""".trimMargin()
}
}

private fun Char.isAsciiAllowed(): Boolean =
this in 'a'..'z' || this in 'A'..'Z' || this in '0'..'9' || this == '_' || this == '-' || this == '.'
Comment thread
devcrocod marked this conversation as resolved.
Comment on lines +28 to +60
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

SEP-986 allows forward slashes (/) and dots (.), but isAsciiAllowed() currently excludes '/'. This causes valid names like "user/profile/update" to be flagged as invalid and also makes the "Allowed characters are..." warning inaccurate. Include '/' in the allowed character set and update the allowed-characters message accordingly.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package io.modelcontextprotocol.kotlin.sdk.server.utils

import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.collections.shouldNotBeEmpty
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

class ToolNameValidationTest {

@ParameterizedTest
@ValueSource(
strings = [
"getUser",
"get_user_profile",
"user-profile-update",
"admin.tools.list",
"DATA_EXPORT_v2.1",
"a",
"Z",
"0",
"a-b.c_d",
],
)
fun `should return no warnings for valid tool names`(name: String) {
validateToolName(name).shouldBeEmpty()
}

@Test
fun `should return no warnings for max length name`() {
validateToolName("a".repeat(128)).shouldBeEmpty()
}
Comment on lines +32 to +35
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This test contradicts SEP-986: max tool name length should be 64, but the test treats 128 as the maximum valid length. Update the test cases to assert 64 is valid and 65 triggers a warning (matching the spec and the validator).

Copilot uses AI. Check for mistakes.

@Test
fun `should warn for empty name`() {
val warnings = validateToolName("")
warnings shouldHaveSize 1
warnings shouldContain "Tool name cannot be empty"
}

@Test
fun `should warn for name exceeding max length`() {
val warnings = validateToolName("a".repeat(129))
warnings.shouldNotBeEmpty()
warnings.any { "exceeds maximum length" in it }.shouldBeTrue()
}

@ParameterizedTest
@ValueSource(strings = ["get user profile", "my tool"])
fun `should warn for names with spaces`(name: String) {
val warnings = validateToolName(name)
warnings shouldContain "Tool name contains spaces, which may cause parsing issues"
}

@ParameterizedTest
@ValueSource(strings = ["get,user,profile", "a,b"])
fun `should warn for names with commas`(name: String) {
val warnings = validateToolName(name)
warnings shouldContain "Tool name contains commas, which may cause parsing issues"
}

@ParameterizedTest
@ValueSource(strings = ["user/profile/update", "a/b"])
fun `should warn for names with forward slashes`(name: String) {
val warnings = validateToolName(name)
warnings.any { "invalid characters" in it }.shouldBeTrue()
}
Comment on lines +65 to +70
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

SEP-986 explicitly allows forward slashes (/), but this test expects names containing '/' to produce an "invalid characters" warning. Once '/' is allowed in the validator, this test should instead assert no warnings for slash-separated tool names (and optionally add a slash example to the "valid tool names" parameter set).

Copilot uses AI. Check for mistakes.

@ParameterizedTest
@ValueSource(strings = ["user@domain.com", "tool#1", "tool\$name"])
fun `should warn for names with special characters`(name: String) {
val warnings = validateToolName(name)
warnings.any { "invalid characters" in it }.shouldBeTrue()
warnings shouldContain "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
}
Comment on lines +72 to +78
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The expected "Allowed characters are..." message omits forward slash (/), but SEP-986 allows it. If you update the validator to allow '/', update this assertion to include '/' in the allowed-character list (or adjust the test to match the final wording).

Copilot uses AI. Check for mistakes.

@Test
fun `should warn for unicode characters`() {
val warnings = validateToolName("user-\u00f1ame")
warnings.any { "invalid characters" in it }.shouldBeTrue()
}

@Test
fun `should warn for name starting with dash`() {
val warnings = validateToolName("-my-tool")
warnings shouldContain "Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
}

@Test
fun `should warn for name ending with dash`() {
val warnings = validateToolName("my-tool-")
warnings shouldContain "Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
}

@Test
fun `should warn for name starting with dot`() {
val warnings = validateToolName(".hidden")
warnings shouldContain "Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
}

@Test
fun `should warn for name ending with dot`() {
val warnings = validateToolName("config.")
warnings shouldContain "Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
}

@Test
fun `should produce multiple warnings for multiple issues`() {
val warnings = validateToolName("my tool,name")
warnings.any { "spaces" in it }.shouldBeTrue()
warnings.any { "commas" in it }.shouldBeTrue()
}

@Test
fun `should not duplicate space or comma in generic invalid characters message`() {
val warnings = validateToolName("my tool")
warnings shouldContain "Tool name contains spaces, which may cause parsing issues"
warnings.none { it.startsWith("Tool name contains invalid characters") }.shouldBeTrue()
}
}
Loading