Skip to content
Draft
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 @@ -218,13 +218,29 @@ class OpenApiOperationTool(
return declared + bodyProps
}

/**
* Substitute every `{param}` placeholder in the path with the caller's argument.
*
* OpenAPI 3 requires path parameters to be `required: true`, so a missing or
* misnamed one is not optional data -- it's a call that cannot possibly reach
* the right resource. We throw here rather than let the literal placeholder
* survive into the URI: a request for `/shows/{id}` sent as-is is guaranteed
* to 404, and that 404 tells the caller nothing about which argument was
* missing. Naming the parameter and the keys actually supplied turns a dead
* end into something the caller (often an LLM) can correct and retry.
*/
private fun resolvePath(path: String, params: Map<String, Any?>): String {
var resolved = path
pathParameterNames().forEach { paramName ->
val value = params[paramName]
if (value != null) {
resolved = resolved.replace("{$paramName}", value.toString())
if (value == null) {
throw IllegalArgumentException(
"Missing required path parameter '$paramName' for operation " +
"'${operation.operationId ?: path}' ($httpMethod $path). " +
"Provided arguments: ${params.keys}",
)
}
resolved = resolved.replace("{$paramName}", value.toString())
}
return resolved
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,79 @@ class OpenApiOperationToolTest {
}
}

// ======================================================================
// Path parameter validation
// ======================================================================

@Nested
inner class PathParameterValidationTests {

/**
* A learned streaming API's `getShow` operation declares `GET /shows/{id}`.
* Calling it without `id` used to leave the literal `{id}` in the built
* URI, which the remote 404'd on every time -- a call that could never
* succeed, surfacing as an opaque HTTP error instead of naming the
* missing argument.
*/
private fun getShowOperation() = Operation().apply {
operationId = "getShow"
parameters = listOf(
Parameter().apply {
name = "id"
`in` = "path"
required = true
schema = StringSchema()
},
)
}

@Test
fun `missing path parameter fails fast instead of sending the literal placeholder`() {
val (tool, server) = createToolWithMock(
PathItem.HttpMethod.GET, "/shows/{id}",
operation = getShowOperation(),
)

val result = tool.call("{}")

assertInstanceOf(Tool.Result.Error::class.java, result)
val error = result as Tool.Result.Error
assertTrue(error.message.contains("id"), "Error should name the missing parameter 'id': ${error.message}")
server.verify() // no request expectations registered -- a request would fail verification
}

@Test
fun `misnamed path parameter fails fast and lists the provided keys`() {
val (tool, server) = createToolWithMock(
PathItem.HttpMethod.GET, "/shows/{id}",
operation = getShowOperation(),
)

val result = tool.call("""{"imdbId": "tt1234567"}""")

assertInstanceOf(Tool.Result.Error::class.java, result)
val error = result as Tool.Result.Error
assertTrue(error.message.contains("id"), "Error should name the missing parameter 'id': ${error.message}")
assertTrue(error.message.contains("imdbId"), "Error should list the provided key 'imdbId': ${error.message}")
server.verify() // no request expectations registered -- a request would fail verification
}

@Test
fun `correctly supplied path parameter still builds the substituted URL`() {
val (tool, server) = createToolWithMock(
PathItem.HttpMethod.GET, "/shows/{id}",
operation = getShowOperation(),
)
server.expect(requestTo("https://api.example.com/shows/tt1234567"))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("""{"id": "tt1234567"}""", MediaType.APPLICATION_JSON))

val result = tool.call("""{"id": "tt1234567"}""")
assertIsText(result, """{"id": "tt1234567"}""")
server.verify()
}
}

// ======================================================================
// URI building
// ======================================================================
Expand Down
Loading