Repository: langchain4j/langchain4j (language: java)

Issue:
[BUG] MCP error in resolving schema specification contains array of any type
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
when converting MCP schema with array of any type,a IllegalArgumentException was thrown,message is 'Unknown element type: '
the original json schema is something like that(a MCP of mysql):
```
{
  "name": "query",
  "description": "Execute a SELECT query",
  "inputSchema": {
    "type": "object",
    "properties": {
      "sql": {
        "type": "string",
        "description": "SQL SELECT query"
      },
      "params": {
        "type": "array",
        "items": {
          "type": [
            "string",
            "number",
            "boolean",
            "null"
          ]
        },
        "description": "Query parameters (optional)"
      }
    },
    "required": [
      "sql"
    ]
  }
}
```
the error may in dev.langchain4j.mcp.client.[ToolSpecificationHelper](https://github.com/langchain4j/langchain4j/blob/1.0.0-beta2/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolSpecificationHelper.java).jsonNodeToJsonSchemaElement
```java
static JsonSchemaElement jsonNodeToJsonSchemaElement(JsonNode node) {
        String nodeType = node.get("type").asText();
        if (nodeType.equals("object")) {
            //...
        } else if (nodeType.equals("string")) {
            if (node.has("enum")) {
               //...
            } else {
               //...
            }
        } else if (nodeType.equals("number")) {
            //...
        } else if (nodeType.equals("integer")) {
            //...
        } else if (nodeType.equals("boolean")) {
            //...
        } else if (nodeType.equals("array")) {
            JsonArraySchema.Builder builder = JsonArraySchema.builder();
            if (node.has("description")) {
                builder.description(node.get("description").asText());
            }
            builder.items(jsonNodeToJsonSchemaElement(node.get("items")));
            return builder.build();
        } else {
            throw new IllegalArgumentException("Unknown element type: " + nodeType);
        }
    }

```
when parsing schema like '{"type": "array","items": {"type": ["string","number","boolean","null"]}',the outer nodeType is 'array' and it's items part ( {"type": ["string","number","boolean","null"]}) was send to next recursion,and in next recursion,get('type') is ArrayNode and asText is an empty string,so IllegalArgumentException happend.
I read some doc about json schema and found it is legal,and can be validated by some online type script validator
(It's means the array elements can be any of the type?)
**Log and Stack trace**
```
java.lang.IllegalArgumentException: Unknown element type: 
	at dev.langchain4j.mcp.client.ToolSpecificationHelper.jsonNodeToJsonSchemaElement(ToolSpecificationHelper.java:118) ~[classes/:na]
	at dev.langchain4j.mcp.client.ToolSpecificationHelper.jsonNodeToJsonSchemaElement(ToolSpecificationHelper.java:105) ~[classes/:na]
	at dev.langchain4j.mcp.client.ToolSpecificationHelper.jsonNodeToJsonSchemaElement(ToolSpecificationHelper.java:63) ~[classes/:na]
	at dev.langchain4j.mcp.client.ToolSpecificationHelper.toolSpecificationListFromMcpResponse(ToolSpecificationHelper.java:34) ~[classes/:na]
	at dev.langchain4j.mcp.client.DefaultMcpClient.listTools(DefaultMcpClient.java:129) ~[classes/:na]
	at dev.langchain4j.mcp.McpToolProvider.provideTools(McpToolProvider.java:34) ~[classes/:na]
	at dev.langchain4j.service.tool.ToolService.executionContext(ToolService.java:109) ~[langchain4j-1.0.0-beta2.jar:na]
	at dev.langchain4j.service.DefaultAiServices$1.invoke(DefaultAiServices.java:179) ~[langchain4j-1.0.0-beta2.jar:na]
	at cn.itcast.langchain.langchain101.l7.mcp.$Proxy77.chat(Unknown Source) ~[na:na]
	at cn.itcast.langchain.langchain101.l7.mcp.MCPController.init(MCPController.java:89) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[na:na]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMethod.invoke(InitDestroyAnnotationBeanPostProcessor.java:457) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:401) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:219) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:423) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:346) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.instantiateSingleton(DefaultListableBeanFactory.java:1155) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingleton(DefaultListableBeanFactory.java:1121) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1056) ~[spring-beans-6.2.3.jar:6.2.3]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:987) ~[spring-context-6.2.3.jar:6.2.3]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:627) ~[spring-context-6.2.3.jar:6.2.3]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.4.3.jar:3.4.3]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) ~[spring-boot-3.4.3.jar:3.4.3]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-3.4.3.jar:3.4.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) ~[spring-boot-3.4.3.jar:3.4.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) ~[spring-boot-3.4.3.jar:3.4.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) ~[spring-boot-3.4.3.jar:3.4.3]
	at cn.itcast.langchain.langchain101.Langchain101Application.main(Langchain101Application.java:10) ~[classes/:na]
```

**To Reproduce**
[mysql-mcp](https://github.com/f4ww4z/mcp-mysql-server)
```java
@RestController
@Slf4j
public class MCPController4Bug {

    @Autowired
    private ChatLanguageModel llm;
    
    @PostConstruct
    void init() {
        var env = new HashMap<String, String>();
        env.put("PATH", "<system PATH(need node installed)>");
        McpTransport hotnewsTp = new StdioMcpTransport.Builder()
                .environment(env)
                .command(List.of("bash", "-c", "npx -y @wopal/mcp-server-hotnews"))
                .logEvents(true)
                .build();
        
        McpClient mcpClient = new DefaultMcpClient.Builder()
                .transport(hotnewsTp)
                .logHandler(m -> log.info("{}", m))
                .build();
        
        var env4mysql = new HashMap<String, String>(env);
        env4mysql.put("MYSQL_HOST", "localhost:3306");
        env4mysql.put("MYSQL_USER", "admin");
        env4mysql.put("MYSQL_PASSWORD", "<password>");
        env4mysql.put("MYSQL_DATABASE", "db4mcp");
        McpTransport mysqlTp = new StdioMcpTransport.Builder()
                .environment(env4mysql)
                .command(List.of("bash", "-c", "npx -y @f4ww4z/mcp-mysql-server"))
                .logEvents(true)
                .build();
        
        McpClient mcpClient2 = new DefaultMcpClient.Builder()
                .transport(mysqlTp)
                .logHandler(m -> log.info("{}", m))
                .build();
        
        ToolProvider toolProvider = McpToolProvider.builder()
                .mcpClients(List.of(
                        mcpClient, 
                        mcpClient2
                        ))
                .build();
        
        var a4mcp = AiServices.builder(Assistant4MCP.class)
            .chatLanguageModel(llm)
            .toolProvider(toolProvider)
            .build();
        
        log.info("{}", a4mcp.chat("""
                help me save hot news to mysql,
                create table if needed,
                here is basic database info：user:admin,password:<password>,host:localhost,port:3306,db:db4mcp
                """));
    }
}
```

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
LLM query hot news and save data to mysql

**Please complete the following information:**
- LangChain4j version:1.0.0-beta2
- LLM(s) used: glm-4-plus
- Java version: 17.0.14
- Spring Boot version (if applicable): 3.4.3
- OS:Ubuntu 22.04.5 LTS

**Additional context**
<!-- Add any other context about the problem here. -->



Discussion / hints:
Hmm, supporting this style where the `type` is an array shouldn't be too hard (we can turn that into a `JsonAnyOfSchema` that contains all the listed types) but I see a bit of a problem with the value "null" inside it, we don't have anything like a `JsonNullSchema`, right, @dliubars ? Do you know how that should be handled?

Fix patch:
diff --git a/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonNullSchema.java b/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonNullSchema.java
new file mode 100644
index 00000000000..0a1e117ac0f
--- /dev/null
+++ b/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonNullSchema.java
@@ -0,0 +1,34 @@
+package dev.langchain4j.model.chat.request.json;
+
+import dev.langchain4j.Experimental;
+
+@Experimental
+public class JsonNullSchema implements JsonSchemaElement {
+
+    public JsonNullSchema() {}
+
+    public JsonNullSchema(Builder builder) {}
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        public JsonNullSchema build() {
+            return new JsonNullSchema(this);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "JsonNullSchema{}";
+    }
+}
diff --git a/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolSpecificationHelper.java b/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolSpecificationHelper.java
index 506838670aa..5e0fb2347f0 100644
--- a/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolSpecificationHelper.java
+++ b/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolSpecificationHelper.java
@@ -2,12 +2,15 @@
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.model.chat.request.json.JsonAnyOfSchema;
 import dev.langchain4j.model.chat.request.json.JsonArraySchema;
 import dev.langchain4j.model.chat.request.json.JsonBooleanSchema;
 import dev.langchain4j.model.chat.request.json.JsonEnumSchema;
 import dev.langchain4j.model.chat.request.json.JsonIntegerSchema;
+import dev.langchain4j.model.chat.request.json.JsonNullSchema;
 import dev.langchain4j.model.chat.request.json.JsonNumberSchema;
 import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
 import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
@@ -15,6 +18,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.StreamSupport;
 
 class ToolSpecificationHelper {
 
@@ -41,70 +45,127 @@ static List<ToolSpecification> toolSpecificationListFromMcpResponse(ArrayNode ar
      * to a JsonSchemaElement object that describes the tool's arguments.
      */
     static JsonSchemaElement jsonNodeToJsonSchemaElement(JsonNode node) {
-        String nodeType = node.get("type").asText();
-        if (nodeType.equals("object")) {
-            JsonObjectSchema.Builder builder = JsonObjectSchema.builder();
-            JsonNode required = node.get("required");
-            if (required != null) {
-                builder.required(toStringArray((ArrayNode) required));
-            }
-            if (node.has("additionalProperties")) {
-                builder.additionalProperties(node.get("additionalProperties").asBoolean(false));
-            }
-            JsonNode description = node.get("description");
-            if (description != null) {
-                builder.description(description.asText());
-            }
-            JsonNode properties = node.get("properties");
-            if (properties != null) {
-                ObjectNode propertiesObject = (ObjectNode) properties;
-                for (Map.Entry<String, JsonNode> property : propertiesObject.properties()) {
-                    builder.addProperty(property.getKey(), jsonNodeToJsonSchemaElement(property.getValue()));
+        if (node.get("type").getNodeType() != JsonNodeType.ARRAY) {
+            String nodeType = node.get("type").asText();
+            if (nodeType.equals("object")) {
+                JsonObjectSchema.Builder builder = JsonObjectSchema.builder();
+                JsonNode required = node.get("required");
+                if (required != null) {
+                    builder.required(toStringArray((ArrayNode) required));
                 }
-            }
-            return builder.build();
-        } else if (nodeType.equals("string")) {
-            if (node.has("enum")) {
-                JsonEnumSchema.Builder builder = JsonEnumSchema.builder();
+                if (node.has("additionalProperties")) {
+                    builder.additionalProperties(
+                            node.get("additionalProperties").asBoolean(false));
+                }
+                JsonNode description = node.get("description");
+                if (description != null) {
+                    builder.description(description.asText());
+                }
+                JsonNode properties = node.get("properties");
+                if (properties != null) {
+                    ObjectNode propertiesObject = (ObjectNode) properties;
+                    for (Map.Entry<String, JsonNode> property : propertiesObject.properties()) {
+                        builder.addProperty(property.getKey(), jsonNodeToJsonSchemaElement(property.getValue()));
+                    }
+                }
+                return builder.build();
+            } else if (nodeType.equals("string")) {
+                if (node.has("enum")) {
+                    JsonEnumSchema.Builder builder = JsonEnumSchema.builder();
+                    if (node.has("description")) {
+                        builder.description(node.get("description").asText());
+                    }
+                    builder.enumValues(toStringArray((ArrayNode) node.get("enum")));
+                    return builder.build();
+                } else {
+                    JsonStringSchema.Builder builder = JsonStringSchema.builder();
+                    if (node.has("description")) {
+                        builder.description(node.get("description").asText());
+                    }
+                    return builder.build();
+                }
+            } else if (nodeType.equals("number")) {
+                JsonNumberSchema.Builder builder = JsonNumberSchema.builder();
                 if (node.has("description")) {
                     builder.description(node.get("description").asText());
                 }
-                builder.enumValues(toStringArray((ArrayNode) node.get("enum")));
                 return builder.build();
-            } else {
-                JsonStringSchema.Builder builder = JsonStringSchema.builder();
+            } else if (nodeType.equals("integer")) {
+                JsonIntegerSchema.Builder builder = JsonIntegerSchema.builder();
                 if (node.has("description")) {
                     builder.description(node.get("description").asText());
                 }
                 return builder.build();
+            } else if (nodeType.equals("boolean")) {
+                JsonBooleanSchema.Builder builder = JsonBooleanSchema.builder();
+                if (node.has("description")) {
+                    builder.description(node.get("description").asText());
+                }
+                return builder.build();
+            } else if (nodeType.equals("array")) {
+                JsonArraySchema.Builder builder = JsonArraySchema.builder();
+                if (node.has("description")) {
+                    builder.description(node.get("description").asText());
+                }
+                builder.items(jsonNodeToJsonSchemaElement(node.get("items")));
+                return builder.build();
+            } else {
+                throw new IllegalArgumentException("Unknown element type: " + nodeType);
             }
-        } else if (nodeType.equals("number")) {
-            JsonNumberSchema.Builder builder = JsonNumberSchema.builder();
-            if (node.has("description")) {
-                builder.description(node.get("description").asText());
-            }
-            return builder.build();
-        } else if (nodeType.equals("integer")) {
-            JsonIntegerSchema.Builder builder = JsonIntegerSchema.builder();
-            if (node.has("description")) {
-                builder.description(node.get("description").asText());
-            }
-            return builder.build();
-        } else if (nodeType.equals("boolean")) {
-            JsonBooleanSchema.Builder builder = JsonBooleanSchema.builder();
-            if (node.has("description")) {
-                builder.description(node.get("description").asText());
-            }
-            return builder.build();
-        } else if (nodeType.equals("array")) {
-            JsonArraySchema.Builder builder = JsonArraySchema.builder();
-            if (node.has("description")) {
-                builder.description(node.get("description").asText());
-            }
-            builder.items(jsonNodeToJsonSchemaElement(node.get("items")));
-            return builder.build();
         } else {
-            throw new IllegalArgumentException("Unknown element type: " + nodeType);
+            // this represents an array with multiple allowed types for items
+            // for example:
+            // "type": "array",
+            //  "items": {
+            //    "type": ["integer", "string", "null"]
+            //  }
+            //
+            // and we transform this into
+            //
+            // "type": "array",
+            // "items": {
+            //   "anyOf": [
+            //       {
+            //           "type": "integer"
+            //       },
+            //       {
+            //           "type": "string"
+            //       },
+            //       {
+            //           "type": "null"
+            //       }
+            //   ]
+            // }
+            JsonAnyOfSchema.Builder anyOf = JsonAnyOfSchema.builder();
+            JsonSchemaElement[] types = StreamSupport.stream(node.get("type").spliterator(), false)
+                    .map(ToolSpecificationHelper::toTypeElement)
+                    .toArray(JsonSchemaElement[]::new);
+            anyOf.anyOf(types);
+            return anyOf.build();
+        }
+    }
+
+    private static JsonSchemaElement toTypeElement(JsonNode node) {
+        if (!node.isTextual()) {
+            throw new IllegalArgumentException(node + " is not a string");
+        }
+        switch (node.textValue()) {
+            case "string":
+                return JsonStringSchema.builder().build();
+            case "number":
+                return JsonNumberSchema.builder().build();
+            case "integer":
+                return JsonIntegerSchema.builder().build();
+            case "boolean":
+                return JsonBooleanSchema.builder().build();
+            case "array":
+                return JsonArraySchema.builder().build();
+            case "object":
+                return JsonObjectSchema.builder().build();
+            case "null":
+                return JsonNullSchema.builder().build();
+            default:
+                throw new IllegalArgumentException("Unsupported type: " + node.textValue());
         }
     }
 
diff --git a/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/InternalOpenAiHelper.java b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/InternalOpenAiHelper.java
index 24403085a49..8c64b821877 100644
--- a/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/InternalOpenAiHelper.java
+++ b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/InternalOpenAiHelper.java
@@ -1,5 +1,22 @@
 package dev.langchain4j.model.openai;
 
+import static dev.langchain4j.internal.Exceptions.illegalArgument;
+import static dev.langchain4j.internal.Utils.isNullOrBlank;
+import static dev.langchain4j.internal.Utils.isNullOrEmpty;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
+import static dev.langchain4j.model.chat.request.ResponseFormat.JSON;
+import static dev.langchain4j.model.chat.request.ResponseFormatType.TEXT;
+import static dev.langchain4j.model.openai.internal.chat.ResponseFormatType.JSON_OBJECT;
+import static dev.langchain4j.model.openai.internal.chat.ResponseFormatType.JSON_SCHEMA;
+import static dev.langchain4j.model.openai.internal.chat.ToolType.FUNCTION;
+import static dev.langchain4j.model.output.FinishReason.CONTENT_FILTER;
+import static dev.langchain4j.model.output.FinishReason.LENGTH;
+import static dev.langchain4j.model.output.FinishReason.STOP;
+import static dev.langchain4j.model.output.FinishReason.TOOL_EXECUTION;
+import static java.lang.String.format;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+
 import dev.langchain4j.agent.tool.ToolExecutionRequest;
 import dev.langchain4j.agent.tool.ToolSpecification;
 import dev.langchain4j.data.image.Image;
@@ -23,6 +40,7 @@
 import dev.langchain4j.model.chat.request.json.JsonBooleanSchema;
 import dev.langchain4j.model.chat.request.json.JsonEnumSchema;
 import dev.langchain4j.model.chat.request.json.JsonIntegerSchema;
+import dev.langchain4j.model.chat.request.json.JsonNullSchema;
 import dev.langchain4j.model.chat.request.json.JsonNumberSchema;
 import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
 import dev.langchain4j.model.chat.request.json.JsonReferenceSchema;
@@ -53,30 +71,12 @@
 import dev.langchain4j.model.openai.internal.shared.Usage;
 import dev.langchain4j.model.output.FinishReason;
 import dev.langchain4j.model.output.Response;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-import static dev.langchain4j.internal.Exceptions.illegalArgument;
-import static dev.langchain4j.internal.Utils.isNullOrBlank;
-import static dev.langchain4j.internal.Utils.isNullOrEmpty;
-import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
-import static dev.langchain4j.model.chat.request.ResponseFormat.JSON;
-import static dev.langchain4j.model.chat.request.ResponseFormatType.TEXT;
-import static dev.langchain4j.model.openai.internal.chat.ResponseFormatType.JSON_OBJECT;
-import static dev.langchain4j.model.openai.internal.chat.ResponseFormatType.JSON_SCHEMA;
-import static dev.langchain4j.model.openai.internal.chat.ToolType.FUNCTION;
-import static dev.langchain4j.model.output.FinishReason.CONTENT_FILTER;
-import static dev.langchain4j.model.output.FinishReason.LENGTH;
-import static dev.langchain4j.model.output.FinishReason.STOP;
-import static dev.langchain4j.model.output.FinishReason.TOOL_EXECUTION;
-import static java.lang.String.format;
-import static java.util.Collections.singletonList;
-import static java.util.stream.Collectors.toList;
-
 public class InternalOpenAiHelper {
 
     static final String DEFAULT_OPENAI_URL = "https://api.openai.com/v1";
@@ -188,7 +188,8 @@ private static dev.langchain4j.model.openai.internal.chat.Content toOpenAiConten
                 .type(ContentType.AUDIO)
                 .inputAudio(InputAudio.builder()
                         .data(ensureNotBlank(audioContent.audio().base64Data(), "audio.base64Data"))
-                        .format(extractSubtype(ensureNotBlank(audioContent.audio().mimeType(), "audio.mimeType")))
+                        .format(extractSubtype(
+                                ensureNotBlank(audioContent.audio().mimeType(), "audio.mimeType")))
                         .build())
                 .build();
     }
@@ -257,10 +258,11 @@ private static dev.langchain4j.model.openai.internal.chat.JsonObjectSchema toOpe
             JsonObjectSchema parameters, boolean strict) {
 
         if (parameters != null) {
-            dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.Builder builder = dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.builder()
-                    .properties(toOpenAiProperties(parameters.properties(), strict))
-                    .required(parameters.required())
-                    .definitions(toOpenAiProperties(parameters.definitions(), strict));
+            dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.Builder builder =
+                    dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.builder()
+                            .properties(toOpenAiProperties(parameters.properties(), strict))
+                            .required(parameters.required())
+                            .definitions(toOpenAiProperties(parameters.definitions(), strict));
             if (strict) {
                 builder
                         // when strict, all fields must be required:
@@ -291,7 +293,8 @@ private static Map<String, dev.langchain4j.model.openai.internal.chat.JsonSchema
             return null;
         }
 
-        Map<String, dev.langchain4j.model.openai.internal.chat.JsonSchemaElement> openAiProperties = new LinkedHashMap<>();
+        Map<String, dev.langchain4j.model.openai.internal.chat.JsonSchemaElement> openAiProperties =
+                new LinkedHashMap<>();
         properties.forEach((key, value) -> openAiProperties.put(key, toOpenAiJsonSchemaElement(value, strict)));
         return openAiProperties;
     }
@@ -300,11 +303,12 @@ private static dev.langchain4j.model.openai.internal.chat.JsonSchemaElement toOp
             JsonSchemaElement jsonSchemaElement, boolean strict) {
 
         if (jsonSchemaElement instanceof JsonObjectSchema jsonObjectSchema) {
-            dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.Builder builder = dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.builder()
-                    .description(jsonObjectSchema.description())
-                    .properties(toOpenAiProperties(jsonObjectSchema.properties(), strict))
-                    .additionalProperties(strict ? Boolean.FALSE : jsonObjectSchema.additionalProperties())
-                    .definitions(toOpenAiProperties(jsonObjectSchema.definitions(), strict));
+            dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.Builder builder =
+                    dev.langchain4j.model.openai.internal.chat.JsonObjectSchema.builder()
+                            .description(jsonObjectSchema.description())
+                            .properties(toOpenAiProperties(jsonObjectSchema.properties(), strict))
+                            .additionalProperties(strict ? Boolean.FALSE : jsonObjectSchema.additionalProperties())
+                            .definitions(toOpenAiProperties(jsonObjectSchema.definitions(), strict));
             if (jsonObjectSchema.required() != null) {
                 builder.required(jsonObjectSchema.required());
             }
@@ -348,6 +352,9 @@ private static dev.langchain4j.model.openai.internal.chat.JsonSchemaElement toOp
             return dev.langchain4j.model.openai.internal.chat.JsonReferenceSchema.builder()
                     .reference("#/$defs/" + jsonReferenceSchema.reference())
                     .build();
+        } else if (jsonSchemaElement instanceof JsonNullSchema jsonNullSchema) {
+            return dev.langchain4j.model.openai.internal.chat.JsonNullSchema.builder()
+                    .build();
         } else if (jsonSchemaElement instanceof JsonAnyOfSchema) {
             JsonAnyOfSchema jsonAnyOfSchema = (JsonAnyOfSchema) jsonSchemaElement;
             return dev.langchain4j.model.openai.internal.chat.JsonAnyOfSchema.builder()
@@ -444,7 +451,8 @@ public static FinishReason finishReasonFrom(String openAiFinishReason) {
         }
     }
 
-    static dev.langchain4j.model.openai.internal.chat.ResponseFormat toOpenAiResponseFormat(ResponseFormat responseFormat, Boolean strict) {
+    static dev.langchain4j.model.openai.internal.chat.ResponseFormat toOpenAiResponseFormat(
+            ResponseFormat responseFormat, Boolean strict) {
         if (responseFormat == null || responseFormat.type() == TEXT) {
             return null;
         }
@@ -460,12 +468,13 @@ static dev.langchain4j.model.openai.internal.chat.ResponseFormat toOpenAiRespons
                         "For OpenAI, the root element of the JSON Schema must be a JsonObjectSchema, but it was: "
                                 + jsonSchema.rootElement().getClass());
             }
-            dev.langchain4j.model.openai.internal.chat.JsonSchema openAiJsonSchema = dev.langchain4j.model.openai.internal.chat.JsonSchema.builder()
-                    .name(jsonSchema.name())
-                    .strict(strict)
-                    .schema((dev.langchain4j.model.openai.internal.chat.JsonObjectSchema)
-                            toOpenAiJsonSchemaElement(jsonSchema.rootElement(), strict))
-                    .build();
+            dev.langchain4j.model.openai.internal.chat.JsonSchema openAiJsonSchema =
+                    dev.langchain4j.model.openai.internal.chat.JsonSchema.builder()
+                            .name(jsonSchema.name())
+                            .strict(strict)
+                            .schema((dev.langchain4j.model.openai.internal.chat.JsonObjectSchema)
+                                    toOpenAiJsonSchemaElement(jsonSchema.rootElement(), strict))
+                            .build();
             return dev.langchain4j.model.openai.internal.chat.ResponseFormat.builder()
                     .type(JSON_SCHEMA)
                     .jsonSchema(openAiJsonSchema)
diff --git a/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonAnyOfSchema.java b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonAnyOfSchema.java
index 583cc530a6a..f10c1385626 100644
--- a/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonAnyOfSchema.java
+++ b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonAnyOfSchema.java
@@ -7,7 +7,6 @@
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
-
 import java.util.List;
 import java.util.Objects;
 
@@ -18,11 +17,12 @@ public class JsonAnyOfSchema extends JsonSchemaElement {
 
     @JsonProperty
     private final String description;
+
     @JsonProperty("anyOf")
     private final List<JsonSchemaElement> anyOf;
 
     public JsonAnyOfSchema(Builder builder) {
-        super(null);
+        super((String) null);
         this.description = builder.description;
         this.anyOf = builder.anyOf;
     }
@@ -30,13 +30,11 @@ public JsonAnyOfSchema(Builder builder) {
     @Override
     public boolean equals(final Object another) {
         if (this == another) return true;
-        return another instanceof JsonAnyOfSchema
-                && equalTo((JsonAnyOfSchema) another);
+        return another instanceof JsonAnyOfSchema && equalTo((JsonAnyOfSchema) another);
     }
 
     private boolean equalTo(final JsonAnyOfSchema another) {
-        return Objects.equals(description, another.description)
-                && Objects.equals(anyOf, another.anyOf);
+        return Objects.equals(description, another.description) && Objects.equals(anyOf, another.anyOf);
     }
 
     @Override
@@ -49,13 +47,10 @@ public int hashCode() {
 
     @Override
     public String toString() {
-        return "JsonAnyOfSchema{" +
-                "description='" + description + '\'' +
-                ", anyOf=" + anyOf +
-                '}';
+        return "JsonAnyOfSchema{" + "description='" + description + '\'' + ", anyOf=" + anyOf + '}';
     }
 
-    public static Builder builder(){
+    public static Builder builder() {
         return new Builder();
     }
 
diff --git a/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonNullSchema.java b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonNullSchema.java
new file mode 100644
index 00000000000..1e39ea3d116
--- /dev/null
+++ b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonNullSchema.java
@@ -0,0 +1,47 @@
+package dev.langchain4j.model.openai.internal.chat;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
+@JsonDeserialize(builder = JsonNullSchema.Builder.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class JsonNullSchema extends JsonSchemaElement {
+
+    public JsonNullSchema(Builder builder) {
+        super("null");
+    }
+
+    @Override
+    public boolean equals(Object another) {
+        if (this == another) return true;
+        return another instanceof JsonNullSchema && equalTo((JsonNullSchema) another);
+    }
+
+    private boolean equalTo(JsonNullSchema another) {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "JsonNullSchema{}";
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @JsonPOJOBuilder(withPrefix = "")
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class Builder {
+
+        public JsonNullSchema build() {
+            return new JsonNullSchema(this);
+        }
+    }
+}
diff --git a/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonReferenceSchema.java b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonReferenceSchema.java
index 5f3d3073fee..ba4080a76cb 100644
--- a/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonReferenceSchema.java
+++ b/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/internal/chat/JsonReferenceSchema.java
@@ -7,7 +7,6 @@
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
-
 import java.util.Objects;
 
 @JsonDeserialize(builder = JsonReferenceSchema.Builder.class)
@@ -19,15 +18,14 @@ public class JsonReferenceSchema extends JsonSchemaElement {
     private final String reference;
 
     public JsonReferenceSchema(Builder builder) {
-        super(null);
+        super((String) null);
         this.reference = builder.reference;
     }
 
     @Override
     public boolean equals(Object another) {
         if (this == another) return true;
-        return another instanceof JsonReferenceSchema
-                && equalTo((JsonReferenceSchema) another);
+        return another instanceof JsonReferenceSchema && equalTo((JsonReferenceSchema) another);
     }
 
     private boolean equalTo(JsonReferenceSchema another) {
@@ -43,9 +41,7 @@ public int hashCode() {
 
     @Override
     public String toString() {
-        return "JsonReferenceSchema{" +
-                "reference=" + reference +
-                "}";
+        return "JsonReferenceSchema{" + "reference=" + reference + "}";
     }
 
     public static Builder builder() {


Test patch:
diff --git a/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/ToolSpecificationHelperTest.java b/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/ToolSpecificationHelperTest.java
index bd490275156..03172417cc8 100644
--- a/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/ToolSpecificationHelperTest.java
+++ b/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/ToolSpecificationHelperTest.java
@@ -6,10 +6,12 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.model.chat.request.json.JsonAnyOfSchema;
 import dev.langchain4j.model.chat.request.json.JsonArraySchema;
 import dev.langchain4j.model.chat.request.json.JsonBooleanSchema;
 import dev.langchain4j.model.chat.request.json.JsonEnumSchema;
 import dev.langchain4j.model.chat.request.json.JsonIntegerSchema;
+import dev.langchain4j.model.chat.request.json.JsonNullSchema;
 import dev.langchain4j.model.chat.request.json.JsonNumberSchema;
 import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
 import dev.langchain4j.model.chat.request.json.JsonStringSchema;
@@ -189,4 +191,54 @@ void toolWithNoParams() throws JsonProcessingException {
         JsonObjectSchema parameters = toolSpecification.parameters();
         assertThat(parameters.properties()).isEmpty();
     }
+
+    @Test
+    void arrayWithMultipleAllowedTypes() throws JsonProcessingException {
+        String text =
+                """
+                        [{
+                          "name": "query",
+                          "description": "Execute a SELECT query",
+                          "inputSchema": {
+                            "type": "object",
+                            "properties": {
+                              "sql": {
+                                "type": "string",
+                                "description": "SQL SELECT query"
+                              },
+                              "params": {
+                                "type": "array",
+                                "items": {
+                                  "type": [
+                                    "string",
+                                    "number",
+                                    "boolean",
+                                    "null",
+                                    "integer"
+                                  ]
+                                },
+                                "description": "Query parameters (optional)"
+                              }
+                            },
+                            "required": [
+                              "sql"
+                            ]
+                          }
+                        }]
+                        """;
+        ArrayNode json = OBJECT_MAPPER.readValue(text, ArrayNode.class);
+        List<ToolSpecification> toolSpecifications = ToolSpecificationHelper.toolSpecificationListFromMcpResponse(json);
+        assertThat(toolSpecifications).hasSize(1);
+        ToolSpecification toolSpecification = toolSpecifications.get(0);
+        JsonObjectSchema parameters = toolSpecification.parameters();
+        JsonArraySchema params = (JsonArraySchema) parameters.properties().get("params");
+        assertThat(params.description()).isEqualTo("Query parameters (optional)");
+        assertThat(params.items()).isInstanceOf(JsonAnyOfSchema.class);
+        JsonAnyOfSchema anyOf = (JsonAnyOfSchema) params.items();
+        assertThat(anyOf.anyOf().get(0)).isInstanceOf(JsonStringSchema.class);
+        assertThat(anyOf.anyOf().get(1)).isInstanceOf(JsonNumberSchema.class);
+        assertThat(anyOf.anyOf().get(2)).isInstanceOf(JsonBooleanSchema.class);
+        assertThat(anyOf.anyOf().get(3)).isInstanceOf(JsonNullSchema.class);
+        assertThat(anyOf.anyOf().get(4)).isInstanceOf(JsonIntegerSchema.class);
+    }
 }
diff --git a/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpPromptsTestBase.java b/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpPromptsTestBase.java
index 65bd8ba5dc3..8826f49a8ea 100644
--- a/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpPromptsTestBase.java
+++ b/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpPromptsTestBase.java
@@ -7,6 +7,7 @@
 import dev.langchain4j.mcp.client.McpClient;
 import dev.langchain4j.mcp.client.McpEmbeddedResource;
 import dev.langchain4j.mcp.client.McpException;
+import dev.langchain4j.mcp.client.McpGetPromptResult;
 import dev.langchain4j.mcp.client.McpImageContent;
 import dev.langchain4j.mcp.client.McpPrompt;
 import dev.langchain4j.mcp.client.McpPromptArgument;
@@ -15,7 +16,6 @@
 import dev.langchain4j.mcp.client.McpResourceContents;
 import dev.langchain4j.mcp.client.McpRole;
 import dev.langchain4j.mcp.client.McpTextContent;
-import dev.langchain4j.mcp.client.McpGetPromptResult;
 import java.util.List;
 import java.util.Map;
 import org.junit.jupiter.api.Test;
diff --git a/langchain4j-open-ai/src/test/java/dev/langchain4j/model/openai/OpenAiToolSpecificationTest.java b/langchain4j-open-ai/src/test/java/dev/langchain4j/model/openai/OpenAiToolSpecificationTest.java
new file mode 100644
index 00000000000..56622c8ba0a
--- /dev/null
+++ b/langchain4j-open-ai/src/test/java/dev/langchain4j/model/openai/OpenAiToolSpecificationTest.java
@@ -0,0 +1,82 @@
+package dev.langchain4j.model.openai;
+
+import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.request.ChatRequest;
+import dev.langchain4j.model.chat.request.json.JsonAnyOfSchema;
+import dev.langchain4j.model.chat.request.json.JsonArraySchema;
+import dev.langchain4j.model.chat.request.json.JsonBooleanSchema;
+import dev.langchain4j.model.chat.request.json.JsonIntegerSchema;
+import dev.langchain4j.model.chat.request.json.JsonNullSchema;
+import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
+public class OpenAiToolSpecificationTest {
+
+    OpenAiChatModel model = OpenAiChatModel.builder()
+            .baseUrl(System.getenv("OPENAI_BASE_URL"))
+            .apiKey(System.getenv("OPENAI_API_KEY"))
+            .modelName(GPT_4_O_MINI)
+            .strictJsonSchema(true)
+            .logRequests(true)
+            .logResponses(true)
+            .build();
+
+    @Test
+    void should_call_tool_with_array_parameter_with_multiple_allowed_types() throws JsonProcessingException {
+        // create a tool whose parameter 'param1' is an array with multiple allowed types:
+        // "param1" : {
+        //   "type" : "array",
+        //   "items": {
+        //     "anyOf": [
+        //         {
+        //             "type": "boolean"
+        //         },
+        //         {
+        //             "type": "null"
+        //         },
+        //         {
+        //             "type": "integer"
+        //         }
+        //     ]
+        //    }
+        //  }
+        // verify that the model can properly pass arguments to this tool
+        JsonAnyOfSchema arrayItems = JsonAnyOfSchema.builder()
+                .anyOf(new JsonBooleanSchema(), new JsonNullSchema(), new JsonIntegerSchema())
+                .build();
+        JsonObjectSchema params = JsonObjectSchema.builder()
+                .addProperty(
+                        "param1", JsonArraySchema.builder().items(arrayItems).build())
+                .required(List.of("param1"))
+                .build();
+        ToolSpecification toolSpecification =
+                ToolSpecification.builder().name("function1").parameters(params).build();
+
+        UserMessage userMessage = UserMessage.from(
+                """
+                Call function1 with param1=[3, null, 'abc',true].
+                """);
+
+        ChatRequest chatRequest = ChatRequest.builder()
+                .messages(userMessage)
+                .toolSpecifications(toolSpecification)
+                .build();
+
+        ChatResponse chatResponse = model.chat(chatRequest);
+
+        assertThat(chatResponse.aiMessage().toolExecutionRequests()).hasSize(1);
+        ToolExecutionRequest request =
+                chatResponse.aiMessage().toolExecutionRequests().get(0);
+        assertThat(request.arguments()).isEqualTo("{\"param1\":[3,null,\"abc\",true]}");
+    }
+}