Skip to content
Open
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
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -17,6 +18,7 @@
* more information.
*/
public class InputSchema {
@JsonAdapter(JsonSchemaTypeAdapter.class)
private String type;
private Map<String, InputSchemaPropertyValue> properties;
private List<String> required;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> {

@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();
Comment on lines +29 to +36
}

@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();
}
Comment on lines +64 to +66
}
in.endArray();

return concreteType != null ? concreteType : nullTypePresent ? "null" : null;
}
}