mbxai 1.4.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mbxai-1.4.0 → mbxai-1.6.0}/.gitignore +2 -1
- {mbxai-1.4.0 → mbxai-1.6.0}/PKG-INFO +1 -1
- {mbxai-1.4.0 → mbxai-1.6.0}/pyproject.toml +2 -2
- {mbxai-1.4.0 → mbxai-1.6.0}/setup.py +1 -1
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/__init__.py +1 -1
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/mcp/server.py +4 -7
- mbxai-1.6.0/src/mbxai/tools/types.py +245 -0
- mbxai-1.6.0/tests/test_mcp_tool_registration.py +221 -0
- mbxai-1.6.0/tests/test_real_mcp_schema.py +308 -0
- mbxai-1.6.0/tests/test_schema_conversion.py +277 -0
- mbxai-1.4.0/src/mbxai/tools/types.py +0 -147
- {mbxai-1.4.0 → mbxai-1.6.0}/LICENSE +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/README.md +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/core.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/mcp/mcp_client_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/mcp/mcp_server_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/openrouter_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/parse_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/parse_tool_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/request.json +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/response.json +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/send_request.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/examples/tool_client_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/mcp/__init__.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/mcp/client.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/mcp/example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/openrouter/__init__.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/openrouter/client.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/openrouter/config.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/openrouter/models.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/openrouter/schema.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/tools/__init__.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/tools/client.py +0 -0
- {mbxai-1.4.0 → mbxai-1.6.0}/src/mbxai/tools/example.py +0 -0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "mbxai"
|
7
|
-
version = "1.
|
7
|
+
version = "1.6.0"
|
8
8
|
authors = [
|
9
9
|
{ name = "MBX AI" }
|
10
10
|
]
|
@@ -82,6 +82,6 @@ strict_equality = true
|
|
82
82
|
|
83
83
|
[dependency-groups]
|
84
84
|
dev = [
|
85
|
-
"build>=1.
|
85
|
+
"build>=1.6.0.post1",
|
86
86
|
"twine>=6.1.0",
|
87
87
|
]
|
@@ -31,7 +31,7 @@ class MCPServer:
|
|
31
31
|
self.app = FastAPI(
|
32
32
|
title=self.name,
|
33
33
|
description=self.description,
|
34
|
-
version="1.
|
34
|
+
version="1.6.0",
|
35
35
|
)
|
36
36
|
|
37
37
|
# Initialize MCP server
|
@@ -74,13 +74,10 @@ class MCPServer:
|
|
74
74
|
tools = await self.mcp_server.list_tools()
|
75
75
|
tool_metadata = tools[-1]
|
76
76
|
|
77
|
-
#
|
77
|
+
# Use the raw inputSchema from FastMCP - it will be processed later by convert_to_strict_schema
|
78
|
+
# when the tool is converted to OpenAI function format
|
78
79
|
inputSchema = tool_metadata.inputSchema
|
79
|
-
|
80
|
-
if "$ref" in inputSchema:
|
81
|
-
ref = inputSchema["$ref"].split("/")[-1]
|
82
|
-
inputSchema = tool_metadata.inputSchema.get("$defs", {}).get(ref, {})
|
83
|
-
|
80
|
+
|
84
81
|
# Create and store Tool instance
|
85
82
|
self._tools[tool_metadata.name] = Tool(
|
86
83
|
name=tool_metadata.name,
|
@@ -0,0 +1,245 @@
|
|
1
|
+
"""
|
2
|
+
Type definitions for the tools package.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Callable
|
6
|
+
from pydantic import BaseModel, Field
|
7
|
+
import logging
|
8
|
+
import json
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
def _resolve_ref(schema: dict[str, Any], ref: str, root_schema: dict[str, Any]) -> dict[str, Any]:
|
13
|
+
"""Resolve a $ref to its actual schema definition.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
schema: The current schema object
|
17
|
+
ref: The reference string (e.g., "#/$defs/MyType")
|
18
|
+
root_schema: The root schema containing $defs
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
The resolved schema definition with all nested $refs also resolved
|
22
|
+
"""
|
23
|
+
if ref.startswith("#/"):
|
24
|
+
# Remove the "#/" prefix and split the path
|
25
|
+
path_parts = ref[2:].split("/")
|
26
|
+
current = root_schema
|
27
|
+
|
28
|
+
# Navigate through the path
|
29
|
+
for part in path_parts:
|
30
|
+
if isinstance(current, dict) and part in current:
|
31
|
+
current = current[part]
|
32
|
+
else:
|
33
|
+
logger.warning(f"Could not resolve $ref: {ref}")
|
34
|
+
return {"type": "object", "additionalProperties": False}
|
35
|
+
|
36
|
+
if isinstance(current, dict):
|
37
|
+
# Recursively process the resolved schema to handle nested $refs
|
38
|
+
return _convert_property_to_strict(current, root_schema)
|
39
|
+
else:
|
40
|
+
return {"type": "object", "additionalProperties": False}
|
41
|
+
else:
|
42
|
+
logger.warning(f"Unsupported $ref format: {ref}")
|
43
|
+
return {"type": "object", "additionalProperties": False}
|
44
|
+
|
45
|
+
def _convert_property_to_strict(prop: dict[str, Any], root_schema: dict[str, Any]) -> dict[str, Any]:
|
46
|
+
"""Convert a single property to strict format, recursively handling nested objects and arrays.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
prop: The property definition to convert
|
50
|
+
root_schema: The root schema containing $defs
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
A strict format property definition with all $refs resolved
|
54
|
+
"""
|
55
|
+
# Handle $ref resolution first - this may return a completely different schema
|
56
|
+
if "$ref" in prop:
|
57
|
+
# Get the referenced schema path
|
58
|
+
ref = prop["$ref"]
|
59
|
+
if ref.startswith("#/"):
|
60
|
+
path_parts = ref[2:].split("/")
|
61
|
+
current = root_schema
|
62
|
+
|
63
|
+
# Navigate through the path to get the referenced schema
|
64
|
+
for part in path_parts:
|
65
|
+
if isinstance(current, dict) and part in current:
|
66
|
+
current = current[part]
|
67
|
+
else:
|
68
|
+
logger.warning(f"Could not resolve $ref: {ref}")
|
69
|
+
return {"type": "object", "additionalProperties": False}
|
70
|
+
|
71
|
+
if isinstance(current, dict):
|
72
|
+
# Recursively process the resolved schema
|
73
|
+
return _convert_property_to_strict(current, root_schema)
|
74
|
+
else:
|
75
|
+
return {"type": "object", "additionalProperties": False}
|
76
|
+
else:
|
77
|
+
logger.warning(f"Unsupported $ref format: {ref}")
|
78
|
+
return {"type": "object", "additionalProperties": False}
|
79
|
+
|
80
|
+
# Get the property type, defaulting to string
|
81
|
+
prop_type = prop.get("type", "string")
|
82
|
+
|
83
|
+
# Start with basic property structure
|
84
|
+
new_prop = {
|
85
|
+
"type": prop_type,
|
86
|
+
"description": prop.get("description", f"A {prop_type} parameter")
|
87
|
+
}
|
88
|
+
|
89
|
+
# Handle specific types
|
90
|
+
if prop_type == "object":
|
91
|
+
# Objects need additionalProperties: false and properties
|
92
|
+
new_prop["additionalProperties"] = False
|
93
|
+
new_prop["properties"] = {}
|
94
|
+
new_prop["required"] = []
|
95
|
+
|
96
|
+
# Process nested properties recursively
|
97
|
+
if "properties" in prop:
|
98
|
+
for nested_name, nested_prop in prop["properties"].items():
|
99
|
+
new_prop["properties"][nested_name] = _convert_property_to_strict(nested_prop, root_schema)
|
100
|
+
|
101
|
+
# Copy required fields
|
102
|
+
if "required" in prop:
|
103
|
+
new_prop["required"] = prop["required"]
|
104
|
+
|
105
|
+
elif prop_type == "array":
|
106
|
+
# Arrays must have items definition
|
107
|
+
if "items" in prop:
|
108
|
+
# Recursively process items schema (this could also have $refs)
|
109
|
+
new_prop["items"] = _convert_property_to_strict(prop["items"], root_schema)
|
110
|
+
else:
|
111
|
+
# Default items schema if missing
|
112
|
+
new_prop["items"] = {
|
113
|
+
"type": "string",
|
114
|
+
"description": "Array item"
|
115
|
+
}
|
116
|
+
|
117
|
+
elif prop_type in ["string", "number", "integer", "boolean"]:
|
118
|
+
# For primitive types, copy additional constraints
|
119
|
+
if "enum" in prop:
|
120
|
+
new_prop["enum"] = prop["enum"]
|
121
|
+
if "minimum" in prop:
|
122
|
+
new_prop["minimum"] = prop["minimum"]
|
123
|
+
if "maximum" in prop:
|
124
|
+
new_prop["maximum"] = prop["maximum"]
|
125
|
+
if "pattern" in prop:
|
126
|
+
new_prop["pattern"] = prop["pattern"]
|
127
|
+
if "format" in prop:
|
128
|
+
new_prop["format"] = prop["format"]
|
129
|
+
|
130
|
+
return new_prop
|
131
|
+
|
132
|
+
def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_input_wrapper: bool = False) -> dict[str, Any]:
|
133
|
+
"""Convert a schema to strict format required by OpenAI.
|
134
|
+
|
135
|
+
This function handles:
|
136
|
+
- Resolving all $ref and $defs to inline definitions
|
137
|
+
- Adding required 'items' property for arrays
|
138
|
+
- Ensuring all objects have 'additionalProperties: false'
|
139
|
+
- Recursively processing nested schemas
|
140
|
+
|
141
|
+
Args:
|
142
|
+
schema: The input schema to validate and convert
|
143
|
+
strict: Whether to enforce strict validation with additionalProperties: false
|
144
|
+
keep_input_wrapper: Whether to keep the input wrapper (for MCP tools)
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
A schema in strict format with all references resolved
|
148
|
+
"""
|
149
|
+
if not schema:
|
150
|
+
return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
|
151
|
+
|
152
|
+
# Create a new schema object to ensure we have all required fields
|
153
|
+
strict_schema = {
|
154
|
+
"type": "object",
|
155
|
+
"properties": {},
|
156
|
+
"required": [],
|
157
|
+
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
158
|
+
}
|
159
|
+
|
160
|
+
# Store the root schema for $ref resolution
|
161
|
+
root_schema = schema
|
162
|
+
|
163
|
+
# Handle input wrapper
|
164
|
+
if "properties" in schema and "input" in schema["properties"]:
|
165
|
+
inputSchema = schema["properties"]["input"]
|
166
|
+
|
167
|
+
# If input has a $ref, resolve it
|
168
|
+
if "$ref" in inputSchema:
|
169
|
+
inputSchema = _resolve_ref(inputSchema, inputSchema["$ref"], root_schema)
|
170
|
+
|
171
|
+
if keep_input_wrapper:
|
172
|
+
# Create the input property schema
|
173
|
+
input_prop_schema = {
|
174
|
+
"type": "object",
|
175
|
+
"properties": {},
|
176
|
+
"required": [],
|
177
|
+
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
178
|
+
}
|
179
|
+
|
180
|
+
# Process input properties
|
181
|
+
if "properties" in inputSchema:
|
182
|
+
for prop_name, prop in inputSchema["properties"].items():
|
183
|
+
input_prop_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
184
|
+
|
185
|
+
# Copy over required fields for input schema
|
186
|
+
if "required" in inputSchema:
|
187
|
+
input_prop_schema["required"] = inputSchema["required"]
|
188
|
+
|
189
|
+
# Add the input property to the main schema
|
190
|
+
strict_schema["properties"]["input"] = input_prop_schema
|
191
|
+
|
192
|
+
# Copy over required fields for main schema
|
193
|
+
if "required" in schema:
|
194
|
+
strict_schema["required"] = schema["required"]
|
195
|
+
else:
|
196
|
+
# If not keeping input wrapper, use input schema directly
|
197
|
+
if "properties" in inputSchema:
|
198
|
+
for prop_name, prop in inputSchema["properties"].items():
|
199
|
+
strict_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
200
|
+
|
201
|
+
# Copy over required fields
|
202
|
+
if "required" in inputSchema:
|
203
|
+
strict_schema["required"] = inputSchema["required"]
|
204
|
+
else:
|
205
|
+
# If no input wrapper, use the schema as is
|
206
|
+
if "properties" in schema:
|
207
|
+
for prop_name, prop in schema["properties"].items():
|
208
|
+
strict_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
209
|
+
|
210
|
+
# Copy over required fields
|
211
|
+
if "required" in schema:
|
212
|
+
strict_schema["required"] = schema["required"]
|
213
|
+
|
214
|
+
return strict_schema
|
215
|
+
|
216
|
+
class ToolCall(BaseModel):
|
217
|
+
"""A tool call from the model."""
|
218
|
+
id: str
|
219
|
+
name: str
|
220
|
+
arguments: dict[str, Any]
|
221
|
+
|
222
|
+
class Tool(BaseModel):
|
223
|
+
"""A tool that can be used by the model."""
|
224
|
+
name: str
|
225
|
+
description: str
|
226
|
+
function: Callable[..., Any] | None = None # Make function optional
|
227
|
+
schema: dict[str, Any]
|
228
|
+
|
229
|
+
def to_openai_function(self) -> dict[str, Any]:
|
230
|
+
"""Convert the tool to an OpenAI function definition."""
|
231
|
+
# Ensure schema is in strict format
|
232
|
+
strict_schema = convert_to_strict_schema(self.schema)
|
233
|
+
|
234
|
+
function_def = {
|
235
|
+
"type": "function",
|
236
|
+
"function": {
|
237
|
+
"name": self.name,
|
238
|
+
"description": self.description,
|
239
|
+
"parameters": strict_schema,
|
240
|
+
"strict": True
|
241
|
+
}
|
242
|
+
}
|
243
|
+
|
244
|
+
logger.debug(f"(types) Created function definition for {self.name}: {json.dumps(function_def, indent=2)}")
|
245
|
+
return function_def
|
@@ -0,0 +1,221 @@
|
|
1
|
+
"""
|
2
|
+
Test MCP tool registration flow to identify where $ref schemas are still used.
|
3
|
+
|
4
|
+
This test simulates the server-client registration process to find the bug.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
from typing import Any
|
10
|
+
from pydantic import BaseModel, Field
|
11
|
+
|
12
|
+
# Add the src directory to the path so we can import from mbxai
|
13
|
+
import sys
|
14
|
+
from pathlib import Path
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
16
|
+
|
17
|
+
from mbxai.mcp.client import MCPTool
|
18
|
+
from mbxai.tools.types import convert_to_strict_schema
|
19
|
+
|
20
|
+
|
21
|
+
class MetadataFilter(BaseModel):
|
22
|
+
"""Model for a single metadata filter key-value pair."""
|
23
|
+
key: str = Field(description="The metadata field name to filter by")
|
24
|
+
value: Any = Field(description="The value to filter for")
|
25
|
+
|
26
|
+
|
27
|
+
class ShopwareKnowledgeSearchInput(BaseModel):
|
28
|
+
"""Input model for Shopware knowledge search."""
|
29
|
+
|
30
|
+
query: str = Field(description="The search query")
|
31
|
+
max_results: int = Field(description="Maximum results (1-20)", ge=1, le=20)
|
32
|
+
include_metadata: bool = Field(description="Whether to include metadata")
|
33
|
+
metadata_filter: list[MetadataFilter] = Field(description="List of metadata filters")
|
34
|
+
|
35
|
+
|
36
|
+
def test_mcp_tool_registration_flow():
|
37
|
+
"""Test the exact MCP tool registration flow to identify $ref issues."""
|
38
|
+
|
39
|
+
print("🔍 Testing MCP Tool Registration Flow")
|
40
|
+
print("=" * 50)
|
41
|
+
|
42
|
+
# Step 1: Simulate what FastMCP generates (like what MCP server gets)
|
43
|
+
print("\n1. Simulating FastMCP schema generation...")
|
44
|
+
fastmcp_schema = ShopwareKnowledgeSearchInput.model_json_schema()
|
45
|
+
|
46
|
+
print("FastMCP Raw Schema:")
|
47
|
+
print(json.dumps(fastmcp_schema, indent=2))
|
48
|
+
|
49
|
+
# Create output directory
|
50
|
+
tmp_dir = Path(__file__).parent.parent / "tmp"
|
51
|
+
tmp_dir.mkdir(exist_ok=True)
|
52
|
+
|
53
|
+
# Save FastMCP schema
|
54
|
+
with open(tmp_dir / "fastmcp_raw_schema.json", 'w') as f:
|
55
|
+
json.dump(fastmcp_schema, f, indent=2)
|
56
|
+
|
57
|
+
# Step 2: Simulate what MCP server stores (what goes in Tool.inputSchema)
|
58
|
+
print("\n2. What MCP server stores in Tool.inputSchema...")
|
59
|
+
mcp_server_stored_schema = fastmcp_schema # Server just stores the raw schema
|
60
|
+
|
61
|
+
with open(tmp_dir / "mcp_server_stored_schema.json", 'w') as f:
|
62
|
+
json.dump(mcp_server_stored_schema, f, indent=2)
|
63
|
+
|
64
|
+
# Step 3: Simulate what /tools endpoint returns
|
65
|
+
print("\n3. What /tools endpoint returns...")
|
66
|
+
tools_endpoint_response = {
|
67
|
+
"name": "search_shopware_knowledge",
|
68
|
+
"description": "Search Shopware knowledge base",
|
69
|
+
"inputSchema": mcp_server_stored_schema, # This has $ref!
|
70
|
+
"internal_url": "http://localhost:8000/tools/search_shopware_knowledge/invoke",
|
71
|
+
"service": "shopware-search",
|
72
|
+
"strict": True
|
73
|
+
}
|
74
|
+
|
75
|
+
print("Tools endpoint response:")
|
76
|
+
print(json.dumps(tools_endpoint_response, indent=2))
|
77
|
+
|
78
|
+
with open(tmp_dir / "tools_endpoint_response.json", 'w') as f:
|
79
|
+
json.dump(tools_endpoint_response, f, indent=2)
|
80
|
+
|
81
|
+
# Step 4: Simulate MCPClient creating MCPTool
|
82
|
+
print("\n4. MCPClient creating MCPTool...")
|
83
|
+
try:
|
84
|
+
# This is what MCPClient.register_mcp_server() does
|
85
|
+
mcp_tool = MCPTool(**tools_endpoint_response)
|
86
|
+
|
87
|
+
print("✅ MCPTool created successfully")
|
88
|
+
print(f"MCPTool.inputSchema: {type(mcp_tool.inputSchema)}")
|
89
|
+
|
90
|
+
# Check if inputSchema still has $ref
|
91
|
+
schema_str = json.dumps(mcp_tool.inputSchema)
|
92
|
+
has_ref = "$ref" in schema_str or "$defs" in schema_str
|
93
|
+
print(f"❌ MCPTool.inputSchema still has $ref/$defs: {has_ref}")
|
94
|
+
|
95
|
+
if has_ref:
|
96
|
+
print(" 🚨 PROBLEM: Raw schema with $ref is stored in MCPTool!")
|
97
|
+
|
98
|
+
with open(tmp_dir / "mcp_tool_input_schema.json", 'w') as f:
|
99
|
+
json.dump(mcp_tool.inputSchema, f, indent=2)
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
print(f"❌ Failed to create MCPTool: {e}")
|
103
|
+
return
|
104
|
+
|
105
|
+
# Step 5: Test MCPTool.to_openai_function()
|
106
|
+
print("\n5. Testing MCPTool.to_openai_function()...")
|
107
|
+
try:
|
108
|
+
openai_function = mcp_tool.to_openai_function()
|
109
|
+
|
110
|
+
print("✅ OpenAI function created")
|
111
|
+
|
112
|
+
# Check if the converted schema still has $ref
|
113
|
+
function_schema_str = json.dumps(openai_function["function"]["parameters"])
|
114
|
+
has_ref_after = "$ref" in function_schema_str or "$defs" in function_schema_str
|
115
|
+
|
116
|
+
print(f"❌ Final OpenAI schema still has $ref/$defs: {has_ref_after}")
|
117
|
+
|
118
|
+
if has_ref_after:
|
119
|
+
print(" 🚨 CRITICAL PROBLEM: convert_to_strict_schema didn't resolve $ref!")
|
120
|
+
else:
|
121
|
+
print(" ✅ convert_to_strict_schema successfully resolved $ref")
|
122
|
+
|
123
|
+
# Check if arrays have items
|
124
|
+
def check_arrays_in_schema(schema, path=""):
|
125
|
+
issues = []
|
126
|
+
if isinstance(schema, dict):
|
127
|
+
if schema.get("type") == "array":
|
128
|
+
if "items" not in schema:
|
129
|
+
issues.append(f"Array at {path} missing items")
|
130
|
+
else:
|
131
|
+
print(f" ✅ Array at {path} has items")
|
132
|
+
|
133
|
+
for key, value in schema.items():
|
134
|
+
if key == "properties" and isinstance(value, dict):
|
135
|
+
for prop_name, prop_schema in value.items():
|
136
|
+
issues.extend(check_arrays_in_schema(prop_schema, f"{path}.{prop_name}" if path else prop_name))
|
137
|
+
elif key == "items" and isinstance(value, dict):
|
138
|
+
issues.extend(check_arrays_in_schema(value, f"{path}.items"))
|
139
|
+
return issues
|
140
|
+
|
141
|
+
array_issues = check_arrays_in_schema(openai_function["function"]["parameters"])
|
142
|
+
if array_issues:
|
143
|
+
for issue in array_issues:
|
144
|
+
print(f" ❌ {issue}")
|
145
|
+
|
146
|
+
with open(tmp_dir / "final_openai_function.json", 'w') as f:
|
147
|
+
json.dump(openai_function, f, indent=2)
|
148
|
+
|
149
|
+
print(f"\n📁 Files created in {tmp_dir}:")
|
150
|
+
print(" - fastmcp_raw_schema.json")
|
151
|
+
print(" - mcp_server_stored_schema.json")
|
152
|
+
print(" - tools_endpoint_response.json")
|
153
|
+
print(" - mcp_tool_input_schema.json")
|
154
|
+
print(" - final_openai_function.json")
|
155
|
+
|
156
|
+
return openai_function
|
157
|
+
|
158
|
+
except Exception as e:
|
159
|
+
print(f"❌ Failed to create OpenAI function: {e}")
|
160
|
+
import traceback
|
161
|
+
traceback.print_exc()
|
162
|
+
return None
|
163
|
+
|
164
|
+
def test_direct_schema_conversion():
|
165
|
+
"""Test direct schema conversion to compare with MCP flow."""
|
166
|
+
|
167
|
+
print("\n\n🔧 Testing Direct Schema Conversion (for comparison)")
|
168
|
+
print("=" * 55)
|
169
|
+
|
170
|
+
# Generate the schema
|
171
|
+
schema = ShopwareKnowledgeSearchInput.model_json_schema()
|
172
|
+
|
173
|
+
print("\n1. Direct conversion without input wrapper...")
|
174
|
+
strict_schema = convert_to_strict_schema(schema, strict=True, keep_input_wrapper=False)
|
175
|
+
|
176
|
+
schema_str = json.dumps(strict_schema)
|
177
|
+
has_ref = "$ref" in schema_str or "$defs" in schema_str
|
178
|
+
print(f" Direct conversion has $ref/$defs: {has_ref}")
|
179
|
+
|
180
|
+
print("\n2. Direct conversion with input wrapper...")
|
181
|
+
# Simulate MCP-style wrapper
|
182
|
+
wrapped_schema = {
|
183
|
+
"type": "object",
|
184
|
+
"properties": {
|
185
|
+
"input": schema
|
186
|
+
},
|
187
|
+
"required": ["input"]
|
188
|
+
}
|
189
|
+
|
190
|
+
strict_wrapped = convert_to_strict_schema(wrapped_schema, strict=True, keep_input_wrapper=True)
|
191
|
+
|
192
|
+
wrapped_str = json.dumps(strict_wrapped)
|
193
|
+
has_ref_wrapped = "$ref" in wrapped_str or "$defs" in wrapped_str
|
194
|
+
print(f" Wrapped conversion has $ref/$defs: {has_ref_wrapped}")
|
195
|
+
|
196
|
+
# Save for comparison
|
197
|
+
tmp_dir = Path(__file__).parent.parent / "tmp"
|
198
|
+
with open(tmp_dir / "direct_conversion_no_wrapper.json", 'w') as f:
|
199
|
+
json.dump(strict_schema, f, indent=2)
|
200
|
+
|
201
|
+
with open(tmp_dir / "direct_conversion_with_wrapper.json", 'w') as f:
|
202
|
+
json.dump(strict_wrapped, f, indent=2)
|
203
|
+
|
204
|
+
|
205
|
+
if __name__ == "__main__":
|
206
|
+
try:
|
207
|
+
# Test the MCP registration flow
|
208
|
+
result = test_mcp_tool_registration_flow()
|
209
|
+
|
210
|
+
# Test direct conversion for comparison
|
211
|
+
test_direct_schema_conversion()
|
212
|
+
|
213
|
+
if result:
|
214
|
+
print("\n✅ MCP flow test completed - check the JSON files for details")
|
215
|
+
else:
|
216
|
+
print("\n❌ MCP flow test failed")
|
217
|
+
|
218
|
+
except Exception as e:
|
219
|
+
print(f"\n💥 Test failed with error: {e}")
|
220
|
+
import traceback
|
221
|
+
traceback.print_exc()
|