mbxai 1.0.5__py3-none-any.whl → 1.0.6__py3-none-any.whl

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/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  MBX AI package.
3
3
  """
4
4
 
5
- __version__ = "1.0.5"
5
+ __version__ = "1.0.6"
@@ -1,59 +1,38 @@
1
1
  """Example MCP server implementation."""
2
2
 
3
- from fastapi import FastAPI, HTTPException
3
+ import asyncio
4
+ from typing import Any
4
5
  from pydantic import BaseModel
6
+ from mcp.server.fastmcp import FastMCP
7
+ from mbxai.mcp import MCPServer
8
+ from fastapi import Body
5
9
  import uvicorn
6
- from typing import Any
7
10
 
8
- app = FastAPI()
11
+ # Create a FastMCP instance for this module
12
+ mcp = FastMCP("html-structure-analyser")
9
13
 
10
14
  class ScraperInput(BaseModel):
11
15
  url: str
12
16
 
13
- class ScrapeHtmlArguments(BaseModel):
14
- input: ScraperInput
15
-
16
-
17
- @app.get("/tools")
18
- async def get_tools():
19
- """Return the list of available tools."""
20
- return {
21
- "tools": [
22
- {
23
- "description": "Scrape HTML content from a URL.\n\n This function fetches the HTML content from a given URL using httpx.\n It handles redirects and raises appropriate exceptions for HTTP errors.\n\n Args:\n input (ScrapeHtmlInput): The input containing the URL to scrape\n\n Returns:\n ScrapeHtmlOutput: The HTML content of the page\n\n Raises:\n httpx.HTTPError: If there's an HTTP error while fetching the page\n Exception: For any other unexpected errors\n ",
24
- "input_schema": {
25
- "$defs": {
26
- "ScrapeHtmlInput": {
27
- "properties": {
28
- "url": {
29
- "description": "The URL to scrape HTML from",
30
- "minLength": 1,
31
- "title": "Url",
32
- "type": "string",
33
- }
34
- },
35
- "required": ["url"],
36
- "title": "ScrapeHtmlInput",
37
- "type": "object",
38
- }
39
- },
40
- "properties": {"input": {"$ref": "#/$defs/ScrapeHtmlInput"}},
41
- "required": ["input"],
42
- "title": "scrape_htmlArguments",
43
- "type": "object",
44
- },
45
- "internal_url": "http://localhost:8000/tools/scrape_html/invoke",
46
- "name": "scrape_html",
47
- "service": "html-structure-analyser",
48
- "strict": True,
49
- }
50
- ]
51
- }
52
-
53
-
54
- @app.post("/tools/scrape_html/invoke")
55
- async def scrape_html(arguments: ScrapeHtmlArguments):
56
- sample_html = """
17
+ @mcp.tool()
18
+ async def scrape_html(input: ScraperInput) -> str:
19
+ """Scrape HTML content from a URL.
20
+
21
+ This function fetches the HTML content from a given URL using httpx.
22
+ It handles redirects and raises appropriate exceptions for HTTP errors.
23
+
24
+ Args:
25
+ input: ScraperInput model containing the URL to scrape
26
+
27
+ Returns:
28
+ str: The HTML content of the page
29
+
30
+ Raises:
31
+ httpx.HTTPError: If there's an HTTP error while fetching the page
32
+ Exception: For any other unexpected errors
33
+ """
34
+ # This is a mock implementation that returns sample HTML
35
+ sample_html = f"""
57
36
  <!DOCTYPE html>
58
37
  <html lang="en">
59
38
  <head>
@@ -75,7 +54,7 @@ async def scrape_html(arguments: ScrapeHtmlArguments):
75
54
  <main>
76
55
  <section id="content">
77
56
  <h2>Main Content</h2>
78
- <p>This is a sample HTML page that was scraped from {arguments.input.url}</p>
57
+ <p>This is a sample HTML page that was scraped from {input.url}</p>
79
58
  <article>
80
59
  <h3>Article Title</h3>
81
60
  <p>This is a sample article with some content.</p>
@@ -90,5 +69,50 @@ async def scrape_html(arguments: ScrapeHtmlArguments):
90
69
  """
91
70
  return sample_html
92
71
 
72
+ class CustomMCPServer(MCPServer):
73
+ """Custom MCP server with overridden endpoints."""
74
+
75
+ def _register_endpoints(self) -> None:
76
+ """Register FastAPI endpoints."""
77
+ @self.app.get("/tools")
78
+ async def get_tools():
79
+ """Return the list of available tools."""
80
+ tools = []
81
+ for tool in self._tools.values():
82
+ tool_dict = tool.model_dump(exclude={'function'})
83
+ tool_dict['internal_url'] = f"http://localhost:8000/tools/{tool.name}/invoke"
84
+ tool_dict['service'] = "html-structure-analyser"
85
+ tools.append(tool_dict)
86
+ return {"tools": tools}
87
+
88
+ @self.app.post("/tools/{tool_name}/invoke")
89
+ async def invoke_tool(tool_name: str, arguments: dict[str, Any] = Body(...)):
90
+ """Invoke a specific MCP tool."""
91
+ try:
92
+ result = await self.mcp_server.call_tool(tool_name, arguments=arguments)
93
+ if isinstance(result, list) and len(result) == 1:
94
+ first_item = result[0]
95
+ if hasattr(first_item, "type") and first_item.type == "text":
96
+ return first_item.text
97
+ elif isinstance(result, dict) and result.get("type") == "text":
98
+ return result["text"]
99
+ return result
100
+ except Exception as e:
101
+ return {"error": f"Error invoking tool {tool_name}: {str(e)}"}
102
+
103
+ async def start_server():
104
+ # Create and start the MCP server
105
+ server = CustomMCPServer("html-structure-analyser")
106
+
107
+ # Register the tool with the MCP server
108
+ await server.add_tool(scrape_html)
109
+
110
+ # Create uvicorn config
111
+ config = uvicorn.Config(server.app, host="0.0.0.0", port=8000)
112
+ server = uvicorn.Server(config)
113
+
114
+ # Start the server
115
+ await server.serve()
116
+
93
117
  if __name__ == "__main__":
94
- uvicorn.run(app, host="0.0.0.0", port=8000)
118
+ asyncio.run(start_server())
mbxai/mcp/client.py CHANGED
@@ -16,7 +16,7 @@ T = TypeVar("T", bound=BaseModel)
16
16
 
17
17
  class MCPTool(Tool):
18
18
  """A tool from the MCP server."""
19
- input_schema: dict[str, Any]
19
+ inputSchema: dict[str, Any]
20
20
  internal_url: str
21
21
  strict: bool = True
22
22
  function: Callable[..., Any] | None = None # Make function optional during initialization
@@ -24,7 +24,7 @@ class MCPTool(Tool):
24
24
  def to_openai_function(self) -> dict[str, Any]:
25
25
  """Convert the tool to an OpenAI function definition."""
26
26
  # Convert schema to strict format, keeping input wrapper
27
- strict_schema = convert_to_strict_schema(self.input_schema, strict=self.strict, keep_input_wrapper=True)
27
+ strict_schema = convert_to_strict_schema(self.inputSchema, strict=self.strict, keep_input_wrapper=True)
28
28
 
29
29
  function_def = {
30
30
  "type": "function",
mbxai/mcp/server.py CHANGED
@@ -14,7 +14,7 @@ class Tool(BaseModel):
14
14
  model_config = ConfigDict(strict=True)
15
15
  name: str = Field(description="The name of the tool")
16
16
  description: str = Field(description="The description of what the tool does")
17
- input_schema: dict[str, Any] = Field(description="The input schema for the tool")
17
+ inputSchema: dict[str, Any] = Field(description="The input schema for the tool")
18
18
  strict: bool = Field(default=True, description="Whether the tool response is strictly validated")
19
19
  function: Callable[..., Any] = Field(description="The tool function", exclude=True)
20
20
 
@@ -31,7 +31,7 @@ class MCPServer:
31
31
  self.app = FastAPI(
32
32
  title=self.name,
33
33
  description=self.description,
34
- version="1.0.5",
34
+ version="1.0.6",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -75,17 +75,17 @@ class MCPServer:
75
75
  tool_metadata = tools[-1]
76
76
 
77
77
  # Convert FastMCP schema to our schema format
78
- input_schema = tool_metadata.inputSchema
79
- if isinstance(input_schema, dict):
80
- if "$ref" in input_schema:
81
- ref = input_schema["$ref"].split("/")[-1]
82
- input_schema = tool_metadata.inputSchema.get("$defs", {}).get(ref, {})
78
+ inputSchema = tool_metadata.inputSchema
79
+ if isinstance(inputSchema, dict):
80
+ if "$ref" in inputSchema:
81
+ ref = inputSchema["$ref"].split("/")[-1]
82
+ inputSchema = tool_metadata.inputSchema.get("$defs", {}).get(ref, {})
83
83
 
84
84
  # Create and store Tool instance
85
85
  self._tools[tool_metadata.name] = Tool(
86
86
  name=tool_metadata.name,
87
87
  description=tool_metadata.description,
88
- input_schema=input_schema,
88
+ inputSchema=inputSchema,
89
89
  strict=True,
90
90
  function=tool
91
91
  )
@@ -9,6 +9,7 @@ from .config import OpenRouterConfig
9
9
  import logging
10
10
  import time
11
11
  import asyncio
12
+ import traceback
12
13
  from functools import wraps
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -150,18 +151,20 @@ class OpenRouterClient:
150
151
  OpenRouterError: For other errors
151
152
  """
152
153
  error_msg = str(error)
154
+ stack_trace = traceback.format_exc()
153
155
  logger.error(f"API error during {operation}: {error_msg}")
156
+ logger.error(f"Stack trace:\n{stack_trace}")
154
157
 
155
158
  if isinstance(error, OpenAIError):
156
- raise OpenRouterAPIError(f"API error during {operation}: {error_msg}")
159
+ raise OpenRouterAPIError(f"API error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
157
160
  elif "Connection" in error_msg:
158
- raise OpenRouterConnectionError(f"Connection error during {operation}: {error_msg}")
161
+ raise OpenRouterConnectionError(f"Connection error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
159
162
  elif "Expecting value" in error_msg and "line" in error_msg:
160
163
  # This is a JSON parsing error, likely due to a truncated or malformed response
161
164
  logger.error("JSON parsing error detected. This might be due to a large response or network issues.")
162
- raise OpenRouterAPIError(f"Response parsing error during {operation}. The response might be too large or malformed.")
165
+ raise OpenRouterAPIError(f"Response parsing error during {operation}. The response might be too large or malformed.\nStack trace:\n{stack_trace}")
163
166
  else:
164
- raise OpenRouterError(f"Error during {operation}: {error_msg}")
167
+ raise OpenRouterError(f"Error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
165
168
 
166
169
  @property
167
170
  def model(self) -> str:
@@ -224,7 +227,9 @@ class OpenRouterClient:
224
227
  return response
225
228
 
226
229
  except Exception as e:
230
+ stack_trace = traceback.format_exc()
227
231
  logger.error(f"Error in chat completion: {str(e)}")
232
+ logger.error(f"Stack trace:\n{stack_trace}")
228
233
  logger.error(f"Request details: model={model or self.model}, stream={stream}, kwargs={kwargs}")
229
234
  logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
230
235
 
@@ -306,16 +311,20 @@ class OpenRouterClient:
306
311
  else:
307
312
  response.choices[0].message.parsed = content
308
313
  except Exception as e:
314
+ stack_trace = traceback.format_exc()
309
315
  logger.error(f"Failed to parse message content: {str(e)}")
310
- raise OpenRouterAPIError(f"Failed to parse message content: {str(e)}")
316
+ logger.error(f"Stack trace:\n{stack_trace}")
317
+ raise OpenRouterAPIError(f"Failed to parse message content: {str(e)}\nStack trace:\n{stack_trace}")
311
318
 
312
319
  logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
313
320
 
314
321
  return response
315
322
 
316
323
  except Exception as e:
324
+ stack_trace = traceback.format_exc()
317
325
  logger.debug(f"Raising error: {e}")
318
326
  logger.error(f"Error in parse completion: {str(e)}")
327
+ logger.error(f"Stack trace:\n{stack_trace}")
319
328
  logger.error(f"Request details: model={model or self.model}, response_format={response_format}, kwargs={kwargs}")
320
329
  logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
321
330
 
mbxai/tools/types.py CHANGED
@@ -33,12 +33,12 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
33
33
 
34
34
  # Handle input wrapper
35
35
  if "properties" in schema and "input" in schema["properties"]:
36
- input_schema = schema["properties"]["input"]
36
+ inputSchema = schema["properties"]["input"]
37
37
 
38
38
  # If input has a $ref, resolve it
39
- if "$ref" in input_schema:
40
- ref = input_schema["$ref"].split("/")[-1]
41
- input_schema = schema.get("$defs", {}).get(ref, {})
39
+ if "$ref" in inputSchema:
40
+ ref = inputSchema["$ref"].split("/")[-1]
41
+ inputSchema = schema.get("$defs", {}).get(ref, {})
42
42
 
43
43
  if keep_input_wrapper:
44
44
  # Create the input property schema
@@ -50,8 +50,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
50
50
  }
51
51
 
52
52
  # Copy over input properties
53
- if "properties" in input_schema:
54
- for prop_name, prop in input_schema["properties"].items():
53
+ if "properties" in inputSchema:
54
+ for prop_name, prop in inputSchema["properties"].items():
55
55
  # Create a new property object with only allowed fields
56
56
  new_prop = {
57
57
  "type": prop.get("type", "string"),
@@ -65,8 +65,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
65
65
  input_prop_schema["properties"][prop_name] = new_prop
66
66
 
67
67
  # Copy over required fields for input schema
68
- if "required" in input_schema:
69
- input_prop_schema["required"] = input_schema["required"]
68
+ if "required" in inputSchema:
69
+ input_prop_schema["required"] = inputSchema["required"]
70
70
 
71
71
  # Add the input property to the main schema
72
72
  strict_schema["properties"]["input"] = input_prop_schema
@@ -76,8 +76,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
76
76
  strict_schema["required"] = schema["required"]
77
77
  else:
78
78
  # If not keeping input wrapper, use input schema directly
79
- if "properties" in input_schema:
80
- for prop_name, prop in input_schema["properties"].items():
79
+ if "properties" in inputSchema:
80
+ for prop_name, prop in inputSchema["properties"].items():
81
81
  # Create a new property object with only allowed fields
82
82
  new_prop = {
83
83
  "type": prop.get("type", "string"),
@@ -91,8 +91,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
91
91
  strict_schema["properties"][prop_name] = new_prop
92
92
 
93
93
  # Copy over required fields
94
- if "required" in input_schema:
95
- strict_schema["required"] = input_schema["required"]
94
+ if "required" in inputSchema:
95
+ strict_schema["required"] = inputSchema["required"]
96
96
  else:
97
97
  # If no input wrapper, use the schema as is
98
98
  if "properties" in schema:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Summary: MBX AI SDK
5
5
  Project-URL: Homepage, https://www.mibexx.de
6
6
  Project-URL: Documentation, https://www.mibexx.de
@@ -1,24 +1,24 @@
1
- mbxai/__init__.py,sha256=VEGYuZlohVN0YN1gJ0_0-GSjbEe5CBWzO8GhNk9NVeY,47
1
+ mbxai/__init__.py,sha256=6ZYZaIzWNocWIRG5tf8Lvkh_oYjjmCckLYyTc-rA6Kw,47
2
2
  mbxai/core.py,sha256=WMvmU9TTa7M_m-qWsUew4xH8Ul6xseCZ2iBCXJTW-Bs,196
3
3
  mbxai/examples/openrouter_example.py,sha256=-grXHKMmFLoh-yUIEMc31n8Gg1S7uSazBWCIOWxgbyQ,1317
4
4
  mbxai/examples/parse_example.py,sha256=eCKMJoOl6qwo8sDP6Trc6ncgjPlgTqi5tPE2kB5_P0k,3821
5
5
  mbxai/examples/parse_tool_example.py,sha256=duHN8scI9ZK6XZ5hdiz1Adzyc-_7tH9Ls9qP4S0bf5s,5477
6
6
  mbxai/examples/tool_client_example.py,sha256=9DNaejXLA85dPbExMiv5y76qlFhzOJF9E5EnMOsy_Dc,3993
7
7
  mbxai/examples/mcp/mcp_client_example.py,sha256=R4H-OU5FvGL41cCkTdLa3bocsmVJYQYOcOHRf61nbZc,2822
8
- mbxai/examples/mcp/mcp_server_example.py,sha256=BK2kaKncyeZ9KHvrBBPGZwNATVNgTFkQebNipKPyEdQ,3380
8
+ mbxai/examples/mcp/mcp_server_example.py,sha256=nFfg22Jnc6HMW_ezLO3So1xwDdx2_rItj5CR-y_Nevs,3966
9
9
  mbxai/mcp/__init__.py,sha256=_ek9iYdYqW5saKetj4qDci11jxesQDiHPJRpHMKkxgU,175
10
- mbxai/mcp/client.py,sha256=BuKmKTupzbiLMFghseeYE6Ih6xCesJ8mIMHaXHiNwlo,5206
10
+ mbxai/mcp/client.py,sha256=eXIN2ebprNF5UgM1jb-4JkXmc-5toUhtlBNFKVU7FgY,5204
11
11
  mbxai/mcp/example.py,sha256=oaol7AvvZnX86JWNz64KvPjab5gg1VjVN3G8eFSzuaE,2350
12
- mbxai/mcp/server.py,sha256=vPDSch2s-hsMrI3QDGu7SxNaxwP1kA6ZB5KLNIvmvzU,3462
12
+ mbxai/mcp/server.py,sha256=iRtbFla0RF6aFrmwZtjCipPhlQcWZxKIq0GmNpauR68,3454
13
13
  mbxai/openrouter/__init__.py,sha256=Ito9Qp_B6q-RLGAQcYyTJVWwR2YAZvNqE-HIYXxhtD8,298
14
- mbxai/openrouter/client.py,sha256=XHKEMF0TKej3HxUomhtF_XD-MQwFvvmULxiZZ5r7KVg,14513
14
+ mbxai/openrouter/client.py,sha256=nS8rfiI6jFZ-2lLPZrWXtBAS1_6KdPVe_ptStvRc_T4,15107
15
15
  mbxai/openrouter/config.py,sha256=Ia93s-auim9Sq71eunVDbn9ET5xX2zusXpV4JBdHAzs,3251
16
16
  mbxai/openrouter/models.py,sha256=b3IjjtZAjeGOf2rLsdnCD1HacjTnS8jmv_ZXorc-KJQ,2604
17
17
  mbxai/tools/__init__.py,sha256=ogxrHvgJ7OR62Lmd5x9Eh5d2C0jqWyQis7Zy3yKpZ78,218
18
18
  mbxai/tools/client.py,sha256=h_1fxVDBq57f_OXNsj-TBp6-r367sv6Z5nk1qLFcLO8,14951
19
19
  mbxai/tools/example.py,sha256=1HgKK39zzUuwFbnp3f0ThyWVfA_8P28PZcTwaUw5K78,2232
20
- mbxai/tools/types.py,sha256=yUMvRlWiFy2KnEQLCerh4XioVKMKa2wHtb3fCDybg34,5878
21
- mbxai-1.0.5.dist-info/METADATA,sha256=7LdPENJiCRnKOaxvyvrVhVeQ8mBAjNFLymTuhKTIZVc,4147
22
- mbxai-1.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- mbxai-1.0.5.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
24
- mbxai-1.0.5.dist-info/RECORD,,
20
+ mbxai/tools/types.py,sha256=pAoVuL7nKhvL3Iek0JheGfll4clsABFLl1CNjmiG3No,5866
21
+ mbxai-1.0.6.dist-info/METADATA,sha256=hlVSbjMuNW66535771JuCQKoLi64n1bEWzG-E_fHe2Q,4147
22
+ mbxai-1.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ mbxai-1.0.6.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
24
+ mbxai-1.0.6.dist-info/RECORD,,
File without changes