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.
- {mbxai-0.8.1 → mbxai-0.8.3}/PKG-INFO +1 -1
- {mbxai-0.8.1 → mbxai-0.8.3}/pyproject.toml +1 -1
- {mbxai-0.8.1 → mbxai-0.8.3}/setup.py +1 -1
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/__init__.py +1 -1
- mbxai-0.8.3/src/mbxai/examples/mcp/mcp_client_example.py +76 -0
- mbxai-0.8.3/src/mbxai/examples/mcp/mcp_server_example.py +94 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/client.py +8 -9
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/server.py +1 -1
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/client.py +3 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/types.py +17 -11
- {mbxai-0.8.1 → mbxai-0.8.3}/.gitignore +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/LICENSE +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/README.md +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/core.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/openrouter_example.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/parse_example.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/parse_tool_example.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/examples/tool_client_example.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/__init__.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/mcp/example.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/__init__.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/config.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/openrouter/models.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/__init__.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/client.py +0 -0
- {mbxai-0.8.1 → mbxai-0.8.3}/src/mbxai/tools/example.py +0 -0
@@ -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>© 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.
|
50
|
+
self._http_client = httpx.Client()
|
52
51
|
|
53
|
-
|
54
|
-
"""Enter the
|
52
|
+
def __enter__(self):
|
53
|
+
"""Enter the context."""
|
55
54
|
return self
|
56
55
|
|
57
|
-
|
58
|
-
"""Exit the
|
59
|
-
|
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
|
-
|
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 =
|
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
|
@@ -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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|