From db2b22002f2f658bde2378198d57de54363b8bb3 Mon Sep 17 00:00:00 2001 From: King Star Date: Wed, 1 Jul 2026 06:22:51 +0800 Subject: [PATCH] fix: tolerate nullable MCP schema types --- .../core/lsp/protocol/InputSchemaTests.java | 84 +++++++++++++++++++ .../core/lsp/protocol/InputSchema.java | 2 + .../protocol/InputSchemaPropertyValue.java | 2 + .../lsp/protocol/JsonSchemaTypeAdapter.java | 72 ++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaTests.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/JsonSchemaTypeAdapter.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaTests.java new file mode 100644 index 00000000..ace741bc --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaTests.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +import com.microsoft.copilot.eclipse.core.lsp.mcp.McpServerToolsCollection; + +/** + * Tests for input schema deserialization. + */ +class InputSchemaTests { + + @Test + void testMcpToolInputSchemaAllowsNullableTypeUnion() { + String json = """ + { + "name": "nullable-repro", + "status": "running", + "tools": [ + { + "name": "demo_tool", + "description": "Demo tool with one optional nullable parameter.", + "_status": "enabled", + "inputSchema": { + "type": "object", + "properties": { + "required_arg": { + "type": "string", + "description": "A required argument." + }, + "optional_arg": { + "type": ["string", "null"], + "description": "Optional, nullable." + }, + "optional_count": { + "type": ["null", "number"], + "description": "Optional count." + } + }, + "required": ["required_arg"] + } + } + ] + } + """; + + McpServerToolsCollection server = new Gson().fromJson(json, McpServerToolsCollection.class); + + assertNotNull(server); + assertEquals("nullable-repro", server.getName()); + assertEquals(1, server.getTools().size()); + InputSchemaPropertyValue optionalArg = server.getTools().get(0).getInputSchema().getProperties() + .get("optional_arg"); + assertNotNull(optionalArg); + assertEquals("string", optionalArg.getType()); + InputSchemaPropertyValue optionalCount = server.getTools().get(0).getInputSchema().getProperties() + .get("optional_count"); + assertNotNull(optionalCount); + assertEquals("number", optionalCount.getType()); + } + + @Test + void testInputSchemaAllowsTypeUnion() { + InputSchema schema = new Gson().fromJson("{\"type\":[\"object\",\"null\"]}", InputSchema.class); + + assertNotNull(schema); + assertEquals("object", schema.getType()); + } + + @Test + void testInputSchemaRejectsMultipleConcreteTypes() { + assertThrows(JsonSyntaxException.class, + () -> new Gson().fromJson("{\"type\":[\"string\",\"number\"]}", InputSchema.class)); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchema.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchema.java index 90a3b567..e326376a 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchema.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchema.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Objects; +import com.google.gson.annotations.JsonAdapter; import org.apache.commons.lang3.builder.ToStringBuilder; /** @@ -17,6 +18,7 @@ * more information. */ public class InputSchema { + @JsonAdapter(JsonSchemaTypeAdapter.class) private String type; private Map properties; private List required; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaPropertyValue.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaPropertyValue.java index c50bfe88..35f438cb 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaPropertyValue.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/InputSchemaPropertyValue.java @@ -5,12 +5,14 @@ import java.util.Objects; +import com.google.gson.annotations.JsonAdapter; import org.apache.commons.lang3.builder.ToStringBuilder; /** * Property value for input schema. */ public class InputSchemaPropertyValue { + @JsonAdapter(JsonSchemaTypeAdapter.class) private String type; private String description; private InputSchemaPropertyValue items; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/JsonSchemaTypeAdapter.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/JsonSchemaTypeAdapter.java new file mode 100644 index 00000000..7a7df2e8 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/JsonSchemaTypeAdapter.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.io.IOException; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Gson adapter for JSON Schema type fields. + */ +public class JsonSchemaTypeAdapter extends TypeAdapter { + + @Override + public String read(JsonReader in) throws IOException { + JsonToken token = in.peek(); + if (token == JsonToken.NULL) { + in.nextNull(); + return null; + } + if (token == JsonToken.STRING) { + return in.nextString(); + } + if (token == JsonToken.BOOLEAN) { + return Boolean.toString(in.nextBoolean()); + } + if (token == JsonToken.BEGIN_ARRAY) { + return readTypeArray(in); + } + + return in.nextString(); + } + + @Override + public void write(JsonWriter out, String value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value); + } + } + + private String readTypeArray(JsonReader in) throws IOException { + String concreteType = null; + boolean nullTypePresent = false; + + in.beginArray(); + while (in.hasNext()) { + JsonToken token = in.peek(); + if (token == JsonToken.STRING) { + String type = in.nextString(); + if ("null".equals(type)) { + nullTypePresent = true; + } else if (concreteType == null) { + concreteType = type; + } else if (!concreteType.equals(type)) { + throw new JsonSyntaxException("Multiple non-null JSON Schema types are not supported"); + } + } else { + in.skipValue(); + } + } + in.endArray(); + + return concreteType != null ? concreteType : nullTypePresent ? "null" : null; + } +}