JetBrains / JetBrains/koog

MCP tool integration: oneOf not supported in schema parser, and LLM-generated arguments not coerced

Open
#2,017 3 comments 0 reactions 1 assignee Claimed by @jewoodev View on GitHub
bug
Dominant language
Kotlin
Stars
4.6k
Forks
474
Avg merge
58m
Merged PRs (30d)
1

Description

## Summary

When using MCP servers whose tool schemas use JSON Schema `oneOf` (rather than `anyOf`), the `DefaultMcpToolDescriptorParser` fails to parse parameters correctly. Additionally, even when schemas are parsed, LLMs frequently stringify nested JSON objects in tool call arguments, and the `McpTool` forwards them as-is — causing MCP servers to reject the call.

Both issues are observable with the Obsidian MCP server (`obsidian-mcp`), but affect any MCP server using `oneOf` or nested object parameters.

## Issue 1: `oneOf` not handled by `DefaultMcpToolDescriptorParser`

**Context:** Many MCP servers (notably Obsidian MCP) use JSON Schema `oneOf` for union-type parameters. For example, the `target` parameter in `obsidian_write_note`:

```json
"target": {
"description": "Where the note lives.",
"oneOf": [
{ "type": "object", "properties": { "type": { "const": "path" }, "path": { ... } } },
{ "type": "object", "properties": { "type": { "const": "active" } } },
{ "type": "object", "properties": { "type": { "const": "periodic" }, ... } }
]
}
```

**What happens:** `DefaultMcpToolDescriptorParser.parseParameterType()` checks for `type`, `anyOf`, and `enum` — but **not `oneOf`**. When a parameter has `oneOf` without `type` or `anyOf`, the parser throws:

```
IllegalArgumentException: Parameter must have type property
```

**Expected:** `oneOf` should be treated equivalently to `anyOf` for the purpose of `ToolParameterType.AnyOf` construction.

**Suggested fix in `DefaultMcpToolDescriptorParser.parseParameterType()`:**
```kotlin
// After the existing anyOf check, add:
val oneOf = element["oneOf"]?.jsonArray
if (oneOf != null) {
return ToolParameterType.AnyOf(
types = oneOf.map { it.jsonObject }.map {
ToolParameterDescriptor(
name = "",
description = it["description"]?.jsonPrimitive?.content.orEmpty(),
type = parseParameterType(it.jsonObject)
)
}.toTypedArray()
)
}
```

Or normalize `oneOf` → `anyOf` before parsing.

## Issue 2: `McpTool` does not coerce stringified JSON arguments

**Context:** Even when the schema correctly describes a parameter as an object type (or `anyOf` of objects), LLMs frequently generate the argument as a JSON string containing the object, rather than a proper nested object:

```json
// What the LLM generates:
{ "target": "{\"type\":\"path\",\"path\":\"folder/note.md\"}" }

// What the MCP server expects:
{ "target": {"type": "path", "path": "folder/note.md"} }
```

**What happens:** `McpTool.execute()` calls `args.toKotlinxJsonObject()` and forwards to `mcpClient.callTool()` as-is. The MCP server receives a string where it expects an object, and the call fails silently or throws.

**Expected:** When a parameter's descriptor type is `Object`, `AnyOf`, or `List`, and the received argument is a string that parses as valid JSON, it should be coerced to the parsed structure before forwarding.

**Suggested fix:** Add argument coercion in `McpTool.execute()` (or provide a hook/wrapper point):
```kotlin
override suspend fun execute(args: JSONObject): CallToolResult {
val coerced = coerceStringifiedJsonArgs(args, descriptor)
return mcpClient.callTool(name = descriptor.name, arguments = coerced.toKotlinxJsonObject())
}
```

## Workaround

We're currently working around both issues in our project with:
1. A custom `McpToolDescriptorParser` that converts `oneOf` → `anyOf` before delegating to `DefaultMcpToolDescriptorParser`
2. A custom `Tool` subclass that coerces stringified JSON arguments before calling the MCP client
3. Building the `ToolRegistry` manually (bypassing `McpToolRegistryProvider.fromTransport()`) to use the custom tool class

This works but requires reimplementing `McpTool`'s logic and bypassing the standard registry builder.

## Environment

- Koog version: 0.7.3
- MCP server: `obsidian-mcp` (Obsidian Local REST API)
- Affected tools: `obsidian_write_note`, `obsidian_append_to_note`, `obsidian_get_note`, `obsidian_search_notes`, and others using `oneOf` or nested object params

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.