diff --git a/src/meilisearch_mcp/server.py b/src/meilisearch_mcp/server.py index 6db263f..31301ea 100644 --- a/src/meilisearch_mcp/server.py +++ b/src/meilisearch_mcp/server.py @@ -72,7 +72,11 @@ async def handle_list_tools() -> list[types.Tool]: types.Tool( name="get-connection-settings", description="Get current Meilisearch connection settings", - inputSchema={"type": "object", "properties": {}}, + inputSchema={ + "type": "object", + "properties": {}, + "additionalProperties": False, + }, ), types.Tool( name="update-connection-settings", @@ -80,25 +84,38 @@ async def handle_list_tools() -> list[types.Tool]: inputSchema={ "type": "object", "properties": { - "url": {"type": "string", "optional": True}, - "api_key": {"type": "string", "optional": True}, + "url": {"type": "string"}, + "api_key": {"type": "string"}, }, + "additionalProperties": False, }, ), types.Tool( name="health-check", description="Check Meilisearch server health", - inputSchema={"type": "object", "properties": {}}, + inputSchema={ + "type": "object", + "properties": {}, + "additionalProperties": False, + }, ), types.Tool( name="get-version", description="Get Meilisearch version information", - inputSchema={"type": "object", "properties": {}}, + inputSchema={ + "type": "object", + "properties": {}, + "additionalProperties": False, + }, ), types.Tool( name="get-stats", description="Get database statistics", - inputSchema={"type": "object", "properties": {}}, + inputSchema={ + "type": "object", + "properties": {}, + "additionalProperties": False, + }, ), types.Tool( name="create-index", @@ -107,15 +124,20 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": { "uid": {"type": "string"}, - "primaryKey": {"type": "string", "optional": True}, + "primaryKey": {"type": "string"}, }, "required": ["uid"], + "additionalProperties": False, }, ), types.Tool( name="list-indexes", description="List all Meilisearch indexes", - inputSchema={"type": "object", "properties": {}}, + inputSchema={ + "type": "object", + "properties": {}, + "additionalProperties": False, + }, ), types.Tool( name="delete-index", @@ -124,6 +146,7 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": {"uid": {"type": "string"}}, "required": ["uid"], + "additionalProperties": False, }, ), types.Tool( @@ -133,10 +156,11 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": { "indexUid": {"type": "string"}, - "offset": {"type": "integer", "optional": True}, - "limit": {"type": "integer", "optional": True}, + "offset": {"type": "integer"}, + "limit": {"type": "integer"}, }, "required": ["indexUid"], + "additionalProperties": False, }, ), types.Tool( @@ -146,10 +170,17 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": { "indexUid": {"type": "string"}, - "documents": {"type": "array", "items": {"type": "object"}}, - "primaryKey": {"type": "string", "optional": True}, + "documents": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": True, + }, + }, + "primaryKey": {"type": "string"}, }, "required": ["indexUid", "documents"], + "additionalProperties": False, }, ), types.Tool( @@ -159,6 +190,7 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": {"indexUid": {"type": "string"}}, "required": ["indexUid"], + "additionalProperties": False, }, ), types.Tool( @@ -168,9 +200,13 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": { "indexUid": {"type": "string"}, - "settings": {"type": "object"}, + "settings": { + "type": "object", + "additionalProperties": True, + }, }, "required": ["indexUid", "settings"], + "additionalProperties": False, }, ), types.Tool( @@ -180,17 +216,17 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": { "query": {"type": "string"}, - "indexUid": {"type": "string", "optional": True}, - "limit": {"type": "integer", "optional": True}, - "offset": {"type": "integer", "optional": True}, - "filter": {"type": "string", "optional": True}, + "indexUid": {"type": "string"}, + "limit": {"type": "integer"}, + "offset": {"type": "integer"}, + "filter": {"type": "string"}, "sort": { "type": "array", "items": {"type": "string"}, - "optional": True, }, }, "required": ["query"], + "additionalProperties": False, }, ), types.Tool( @@ -200,6 +236,7 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": {"taskUid": {"type": "integer"}}, "required": ["taskUid"], + "additionalProperties": False, }, ), types.Tool( @@ -208,46 +245,41 @@ async def handle_list_tools() -> list[types.Tool]: inputSchema={ "type": "object", "properties": { - "limit": {"type": "integer", "optional": True}, - "from": {"type": "integer", "optional": True}, - "reverse": {"type": "boolean", "optional": True}, + "limit": {"type": "integer"}, + "from": {"type": "integer"}, + "reverse": {"type": "boolean"}, "batchUids": { "type": "array", "items": {"type": "string"}, - "optional": True, }, "uids": { "type": "array", "items": {"type": "integer"}, - "optional": True, }, "canceledBy": { "type": "array", "items": {"type": "string"}, - "optional": True, }, "types": { "type": "array", "items": {"type": "string"}, - "optional": True, }, "statuses": { "type": "array", "items": {"type": "string"}, - "optional": True, }, "indexUids": { "type": "array", "items": {"type": "string"}, - "optional": True, }, - "afterEnqueuedAt": {"type": "string", "optional": True}, - "beforeEnqueuedAt": {"type": "string", "optional": True}, - "afterStartedAt": {"type": "string", "optional": True}, - "beforeStartedAt": {"type": "string", "optional": True}, - "afterFinishedAt": {"type": "string", "optional": True}, - "beforeFinishedAt": {"type": "string", "optional": True}, + "afterEnqueuedAt": {"type": "string"}, + "beforeEnqueuedAt": {"type": "string"}, + "afterStartedAt": {"type": "string"}, + "beforeStartedAt": {"type": "string"}, + "afterFinishedAt": {"type": "string"}, + "beforeFinishedAt": {"type": "string"}, }, + "additionalProperties": False, }, ), types.Tool( @@ -256,11 +288,12 @@ async def handle_list_tools() -> list[types.Tool]: inputSchema={ "type": "object", "properties": { - "uids": {"type": "string", "optional": True}, - "indexUids": {"type": "string", "optional": True}, - "types": {"type": "string", "optional": True}, - "statuses": {"type": "string", "optional": True}, + "uids": {"type": "string"}, + "indexUids": {"type": "string"}, + "types": {"type": "string"}, + "statuses": {"type": "string"}, }, + "additionalProperties": False, }, ), types.Tool( @@ -269,9 +302,10 @@ async def handle_list_tools() -> list[types.Tool]: inputSchema={ "type": "object", "properties": { - "offset": {"type": "integer", "optional": True}, - "limit": {"type": "integer", "optional": True}, + "offset": {"type": "integer"}, + "limit": {"type": "integer"}, }, + "additionalProperties": False, }, ), types.Tool( @@ -280,12 +314,13 @@ async def handle_list_tools() -> list[types.Tool]: inputSchema={ "type": "object", "properties": { - "description": {"type": "string", "optional": True}, + "description": {"type": "string"}, "actions": {"type": "array", "items": {"type": "string"}}, "indexes": {"type": "array", "items": {"type": "string"}}, - "expiresAt": {"type": "string", "optional": True}, + "expiresAt": {"type": "string"}, }, "required": ["actions", "indexes"], + "additionalProperties": False, }, ), types.Tool( @@ -295,12 +330,17 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": {"key": {"type": "string"}}, "required": ["key"], + "additionalProperties": False, }, ), types.Tool( name="get-health-status", description="Get comprehensive health status of Meilisearch", - inputSchema={"type": "object", "properties": {}}, + inputSchema={ + "type": "object", + "properties": {}, + "additionalProperties": False, + }, ), types.Tool( name="get-index-metrics", @@ -309,12 +349,17 @@ async def handle_list_tools() -> list[types.Tool]: "type": "object", "properties": {"indexUid": {"type": "string"}}, "required": ["indexUid"], + "additionalProperties": False, }, ), types.Tool( name="get-system-info", description="Get system-level information", - inputSchema={"type": "object", "properties": {}}, + inputSchema={ + "type": "object", + "properties": {}, + "additionalProperties": False, + }, ), ] diff --git a/tests/test_mcp_client.py b/tests/test_mcp_client.py index 9a7fd46..063735e 100644 --- a/tests/test_mcp_client.py +++ b/tests/test_mcp_client.py @@ -610,3 +610,105 @@ async def test_delete_index_integration_workflow(self, mcp_server): ) search_after_text = assert_text_content_response(search_after_delete, "Error:") assert "Error:" in search_after_text + + +class TestIssue27OpenAISchemaCompatibility: + """Test for issue #27 - Fix JSON schemas for OpenAI Agent SDK compatibility""" + + async def test_all_schemas_have_additional_properties_false(self, mcp_server): + """Test that all tool schemas include additionalProperties: false for OpenAI compatibility (issue #27)""" + tools = await simulate_list_tools(mcp_server) + + for tool in tools: + schema = tool.inputSchema + assert schema["type"] == "object" + assert ( + "additionalProperties" in schema + ), f"Tool '{tool.name}' missing additionalProperties" + assert ( + schema["additionalProperties"] is False + ), f"Tool '{tool.name}' additionalProperties should be false" + + async def test_array_schemas_have_items_property(self, mcp_server): + """Test that all array schemas include items property for OpenAI compatibility (issue #27)""" + tools = await simulate_list_tools(mcp_server) + + tools_with_arrays = ["add-documents", "search", "get-tasks", "create-key"] + + for tool in tools: + if tool.name in tools_with_arrays: + schema = tool.inputSchema + properties = schema.get("properties", {}) + + for prop_name, prop_schema in properties.items(): + if prop_schema.get("type") == "array": + assert ( + "items" in prop_schema + ), f"Tool '{tool.name}' property '{prop_name}' missing items" + assert isinstance( + prop_schema["items"], dict + ), f"Tool '{tool.name}' property '{prop_name}' items should be object" + + async def test_no_custom_optional_properties(self, mcp_server): + """Test that schemas don't use non-standard 'optional' property (issue #27)""" + tools = await simulate_list_tools(mcp_server) + + for tool in tools: + schema = tool.inputSchema + properties = schema.get("properties", {}) + + for prop_name, prop_schema in properties.items(): + assert ( + "optional" not in prop_schema + ), f"Tool '{tool.name}' property '{prop_name}' uses non-standard 'optional'" + + async def test_specific_add_documents_schema_compliance(self, mcp_server): + """Test add-documents schema specifically mentioned in issue #27""" + tools = await simulate_list_tools(mcp_server) + add_docs_tool = next(tool for tool in tools if tool.name == "add-documents") + + schema = add_docs_tool.inputSchema + + # Verify overall structure + assert schema["type"] == "object" + assert schema["additionalProperties"] is False + assert "properties" in schema + assert "required" in schema + + # Verify documents array property + documents_prop = schema["properties"]["documents"] + assert documents_prop["type"] == "array" + assert ( + "items" in documents_prop + ), "add-documents documents array missing items property" + assert documents_prop["items"]["type"] == "object" + + # Verify required fields + assert "indexUid" in schema["required"] + assert "documents" in schema["required"] + assert "primaryKey" not in schema["required"] # Should be optional + + async def test_openai_compatible_tool_schema_format(self, mcp_server): + """Test that tool schemas follow OpenAI function calling format (issue #27)""" + tools = await simulate_list_tools(mcp_server) + + for tool in tools: + # Verify tool has required OpenAI attributes + assert hasattr(tool, "name") + assert hasattr(tool, "description") + assert hasattr(tool, "inputSchema") + + # Verify schema structure matches OpenAI expectations + schema = tool.inputSchema + assert isinstance(schema, dict) + assert schema.get("type") == "object" + assert "properties" in schema + assert isinstance(schema["properties"], dict) + + # If tool has required parameters, they should be in required array + if "required" in schema: + assert isinstance(schema["required"], list) + + # All required fields should exist in properties + for required_field in schema["required"]: + assert required_field in schema["properties"]