mbxai 0.8.1__tar.gz → 0.8.3__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.
Files changed (26) hide show
  1. {mbxai-0.8.1 → mbxai-0.8.3}/PKG-INFO +1 -1
  2. {mbxai-0.8.1 → mbxai-0.8.3}/pyproject.toml +1 -1
  3. {mbxai-0.8.1 → mbxai-0.8.3}/setup.py +1 -1
  4. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/__init__.py +1 -1
  5. mbxai-0.8.3/src/mbxai/examples/mcp/mcp_client_example.py +76 -0
  6. mbxai-0.8.3/src/mbxai/examples/mcp/mcp_server_example.py +94 -0
  7. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/client.py +8 -9
  8. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/server.py +1 -1
  9. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/client.py +3 -0
  10. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/types.py +17 -11
  11. {mbxai-0.8.1 → mbxai-0.8.3}/.gitignore +0 -0
  12. {mbxai-0.8.1 → mbxai-0.8.3}/LICENSE +0 -0
  13. {mbxai-0.8.1 → mbxai-0.8.3}/README.md +0 -0
  14. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/core.py +0 -0
  15. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/openrouter_example.py +0 -0
  16. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/parse_example.py +0 -0
  17. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/parse_tool_example.py +0 -0
  18. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/tool_client_example.py +0 -0
  19. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/__init__.py +0 -0
  20. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/example.py +0 -0
  21. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/__init__.py +0 -0
  22. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/config.py +0 -0
  23. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/models.py +0 -0
  24. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/__init__.py +0 -0
  25. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/client.py +0 -0
  26. {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 0.8.1
3
+ Version: 0.8.3
4
4
  Summary: MBX AI SDK
5
5
  Project-URL: Homepage, https://www.mibexx.de
6
6
  Project-URL: Documentation, https://www.mibexx.de
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mbxai"
7
- version = "0.8.1"
7
+ version = "0.8.3"
8
8
  authors = [
9
9
  { name = "MBX AI" }
10
10
  ]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="mbxai",
5
- version="0.8.1",
5
+ version="0.8.3",
6
6
  author="MBX AI",
7
7
  description="MBX AI SDK",
8
8
  long_description=open("README.md").read(),
@@ -2,4 +2,4 @@
2
2
  MBX AI package.
3
3
  """
4
4
 
5
- __version__ = "0.8.1"
5
+ __version__ = "0.8.3"
@@ -0,0 +1,76 @@
1
+ """Example usage of the MCP client."""
2
+
3
+ import logging
4
+ import os
5
+ from mbxai.openrouter import OpenRouterClient
6
+ from mbxai.mcp import MCPClient
7
+
8
+ # Configure logging
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def main():
13
+ # Get API key from environment variable
14
+ api_key = os.getenv("OPENROUTER_API_KEY")
15
+ if not api_key:
16
+ logger.error("Please set the OPENROUTER_API_KEY environment variable")
17
+ return
18
+
19
+ try:
20
+ # Initialize the OpenRouter client (required by MCPClient)
21
+ openrouter_client = OpenRouterClient(token=api_key)
22
+
23
+ # Create MCP client
24
+ with MCPClient(openrouter_client) as client:
25
+ # Register the local MCP server
26
+ server_url = "http://localhost:8000"
27
+ try:
28
+ client.register_mcp_server("mcp_server", server_url)
29
+ except Exception as e:
30
+ logger.error(f"Failed to register MCP server: {str(e)}")
31
+ logger.error("Make sure the MCP server is running at http://localhost:8000")
32
+ return
33
+
34
+ # Test chat with tool calls
35
+ messages = [
36
+ {
37
+ "role": "user",
38
+ "content": "Scrape this example url: https://www.google.com"
39
+ }
40
+ ]
41
+
42
+ try:
43
+ # Get the chat response
44
+ response = client.chat(messages=messages)
45
+
46
+ # Print the final response
47
+ if not response:
48
+ logger.error("No response received from the model")
49
+ return
50
+
51
+ if not hasattr(response, 'choices'):
52
+ logger.error(f"Invalid response format - no choices attribute: {response}")
53
+ return
54
+
55
+ if not response.choices:
56
+ logger.error("No choices in response")
57
+ return
58
+
59
+ final_message = response.choices[0].message
60
+ if not final_message:
61
+ logger.error("No message in first choice")
62
+ return
63
+
64
+ logger.info(f"Final response: {final_message.content}")
65
+
66
+ except Exception as e:
67
+ logger.error(f"Error during chat: {str(e)}")
68
+ if hasattr(e, 'response'):
69
+ logger.error(f"Response status: {e.response.status_code if e.response else 'No response'}")
70
+ logger.error(f"Response content: {e.response.text if e.response else 'No content'}")
71
+
72
+ except Exception as e:
73
+ logger.error(f"Error initializing clients: {str(e)}")
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,94 @@
1
+ """Example MCP server implementation."""
2
+
3
+ from fastapi import FastAPI, HTTPException
4
+ from pydantic import BaseModel
5
+ import uvicorn
6
+ from typing import Any
7
+
8
+ app = FastAPI()
9
+
10
+ class ScraperInput(BaseModel):
11
+ url: str
12
+
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 = """
57
+ <!DOCTYPE html>
58
+ <html lang="en">
59
+ <head>
60
+ <meta charset="UTF-8">
61
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
62
+ <title>Sample Page</title>
63
+ </head>
64
+ <body>
65
+ <header>
66
+ <h1>Welcome to Sample Page</h1>
67
+ <nav>
68
+ <ul>
69
+ <li><a href="#home">Home</a></li>
70
+ <li><a href="#about">About</a></li>
71
+ <li><a href="#contact">Contact</a></li>
72
+ </ul>
73
+ </nav>
74
+ </header>
75
+ <main>
76
+ <section id="content">
77
+ <h2>Main Content</h2>
78
+ <p>This is a sample HTML page that was scraped from {arguments.input.url}</p>
79
+ <article>
80
+ <h3>Article Title</h3>
81
+ <p>This is a sample article with some content.</p>
82
+ </article>
83
+ </section>
84
+ </main>
85
+ <footer>
86
+ <p>&copy; 2024 Sample Website</p>
87
+ </footer>
88
+ </body>
89
+ </html>
90
+ """
91
+ return sample_html
92
+
93
+ if __name__ == "__main__":
94
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -3,7 +3,6 @@
3
3
  from typing import Any, TypeVar, Callable
4
4
  import httpx
5
5
  import logging
6
- import asyncio
7
6
  import json
8
7
  from pydantic import BaseModel, Field
9
8
 
@@ -48,15 +47,15 @@ class MCPClient(ToolClient):
48
47
  """Initialize the MCP client."""
49
48
  super().__init__(openrouter_client)
50
49
  self._mcp_servers: dict[str, str] = {}
51
- self._http_client = httpx.AsyncClient()
50
+ self._http_client = httpx.Client()
52
51
 
53
- async def __aenter__(self):
54
- """Enter the async context."""
52
+ def __enter__(self):
53
+ """Enter the context."""
55
54
  return self
56
55
 
57
- async def __aexit__(self, exc_type, exc_val, exc_tb):
58
- """Exit the async context."""
59
- await self._http_client.aclose()
56
+ def __exit__(self, exc_type, exc_val, exc_tb):
57
+ """Exit the context."""
58
+ self._http_client.close()
60
59
 
61
60
  def _create_tool_function(self, tool: MCPTool) -> Callable[..., Any]:
62
61
  """Create a function that invokes an MCP tool."""
@@ -96,12 +95,12 @@ class MCPClient(ToolClient):
96
95
 
97
96
  return tool_function
98
97
 
99
- async def register_mcp_server(self, name: str, base_url: str) -> None:
98
+ def register_mcp_server(self, name: str, base_url: str) -> None:
100
99
  """Register an MCP server and load its tools."""
101
100
  self._mcp_servers[name] = base_url.rstrip("/")
102
101
 
103
102
  # Fetch tools from the server
104
- response = await self._http_client.get(f"{base_url}/tools")
103
+ response = self._http_client.get(f"{base_url}/tools")
105
104
  response_data = response.json()
106
105
 
107
106
  # Extract tools array from response
@@ -31,7 +31,7 @@ class MCPServer:
31
31
  self.app = FastAPI(
32
32
  title=self.name,
33
33
  description=self.description,
34
- version="0.8.1",
34
+ version="0.8.3",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -212,6 +212,9 @@ class OpenRouterClient:
212
212
  }
213
213
 
214
214
  response = self._client.chat.completions.create(**request)
215
+
216
+ logger.info(f"Response: {response}")
217
+
215
218
  logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
216
219
 
217
220
  return response
@@ -21,19 +21,16 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
21
21
  A schema in strict format
22
22
  """
23
23
  if not schema:
24
- return {"type": "object", "properties": {}, "required": []}
24
+ return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
25
25
 
26
26
  # Create a new schema object to ensure we have all required fields
27
27
  strict_schema = {
28
28
  "type": "object",
29
29
  "properties": {},
30
- "required": []
30
+ "required": [],
31
+ "additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
31
32
  }
32
33
 
33
- # Add additionalProperties: false for strict validation
34
- if strict:
35
- strict_schema["additionalProperties"] = False
36
-
37
34
  # Handle input wrapper
38
35
  if "properties" in schema and "input" in schema["properties"]:
39
36
  input_schema = schema["properties"]["input"]
@@ -48,13 +45,10 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
48
45
  input_prop_schema = {
49
46
  "type": "object",
50
47
  "properties": {},
51
- "required": []
48
+ "required": [],
49
+ "additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
52
50
  }
53
51
 
54
- # Add additionalProperties: false for input schema
55
- if strict:
56
- input_prop_schema["additionalProperties"] = False
57
-
58
52
  # Copy over input properties
59
53
  if "properties" in input_schema:
60
54
  for prop_name, prop in input_schema["properties"].items():
@@ -64,6 +58,10 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
64
58
  "description": prop.get("description", f"The {prop_name} parameter")
65
59
  }
66
60
 
61
+ # If the property is an object, ensure it has additionalProperties: false
62
+ if new_prop["type"] == "object":
63
+ new_prop["additionalProperties"] = False
64
+
67
65
  input_prop_schema["properties"][prop_name] = new_prop
68
66
 
69
67
  # Copy over required fields for input schema
@@ -86,6 +84,10 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
86
84
  "description": prop.get("description", f"The {prop_name} parameter")
87
85
  }
88
86
 
87
+ # If the property is an object, ensure it has additionalProperties: false
88
+ if new_prop["type"] == "object":
89
+ new_prop["additionalProperties"] = False
90
+
89
91
  strict_schema["properties"][prop_name] = new_prop
90
92
 
91
93
  # Copy over required fields
@@ -101,6 +103,10 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
101
103
  "description": prop.get("description", f"The {prop_name} parameter")
102
104
  }
103
105
 
106
+ # If the property is an object, ensure it has additionalProperties: false
107
+ if new_prop["type"] == "object":
108
+ new_prop["additionalProperties"] = False
109
+
104
110
  strict_schema["properties"][prop_name] = new_prop
105
111
 
106
112
  # Copy over required fields
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes