mbxai 1.0.5__tar.gz → 1.0.6__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.0.5 → mbxai-1.0.6}/PKG-INFO +1 -1
- {mbxai-1.0.5 → mbxai-1.0.6}/pyproject.toml +1 -1
- {mbxai-1.0.5 → mbxai-1.0.6}/setup.py +1 -1
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/__init__.py +1 -1
- mbxai-1.0.6/src/mbxai/examples/mcp/mcp_server_example.py +118 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/mcp/client.py +2 -2
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/mcp/server.py +8 -8
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/openrouter/client.py +14 -5
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/tools/types.py +12 -12
- mbxai-1.0.5/src/mbxai/examples/mcp/mcp_server_example.py +0 -94
- {mbxai-1.0.5 → mbxai-1.0.6}/.gitignore +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/LICENSE +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/README.md +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/core.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/examples/mcp/mcp_client_example.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/examples/openrouter_example.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/examples/parse_example.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/examples/parse_tool_example.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/examples/tool_client_example.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/mcp/__init__.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/mcp/example.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/openrouter/__init__.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/openrouter/config.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/openrouter/models.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/tools/__init__.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/tools/client.py +0 -0
- {mbxai-1.0.5 → mbxai-1.0.6}/src/mbxai/tools/example.py +0 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
"""Example MCP server implementation."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from typing import Any
|
5
|
+
from pydantic import BaseModel
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
7
|
+
from mbxai.mcp import MCPServer
|
8
|
+
from fastapi import Body
|
9
|
+
import uvicorn
|
10
|
+
|
11
|
+
# Create a FastMCP instance for this module
|
12
|
+
mcp = FastMCP("html-structure-analyser")
|
13
|
+
|
14
|
+
class ScraperInput(BaseModel):
|
15
|
+
url: str
|
16
|
+
|
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"""
|
36
|
+
<!DOCTYPE html>
|
37
|
+
<html lang="en">
|
38
|
+
<head>
|
39
|
+
<meta charset="UTF-8">
|
40
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
41
|
+
<title>Sample Page</title>
|
42
|
+
</head>
|
43
|
+
<body>
|
44
|
+
<header>
|
45
|
+
<h1>Welcome to Sample Page</h1>
|
46
|
+
<nav>
|
47
|
+
<ul>
|
48
|
+
<li><a href="#home">Home</a></li>
|
49
|
+
<li><a href="#about">About</a></li>
|
50
|
+
<li><a href="#contact">Contact</a></li>
|
51
|
+
</ul>
|
52
|
+
</nav>
|
53
|
+
</header>
|
54
|
+
<main>
|
55
|
+
<section id="content">
|
56
|
+
<h2>Main Content</h2>
|
57
|
+
<p>This is a sample HTML page that was scraped from {input.url}</p>
|
58
|
+
<article>
|
59
|
+
<h3>Article Title</h3>
|
60
|
+
<p>This is a sample article with some content.</p>
|
61
|
+
</article>
|
62
|
+
</section>
|
63
|
+
</main>
|
64
|
+
<footer>
|
65
|
+
<p>© 2024 Sample Website</p>
|
66
|
+
</footer>
|
67
|
+
</body>
|
68
|
+
</html>
|
69
|
+
"""
|
70
|
+
return sample_html
|
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
|
+
|
117
|
+
if __name__ == "__main__":
|
118
|
+
asyncio.run(start_server())
|
@@ -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
|
-
|
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.
|
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",
|
@@ -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
|
-
|
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.
|
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
|
-
|
79
|
-
if isinstance(
|
80
|
-
if "$ref" in
|
81
|
-
ref =
|
82
|
-
|
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
|
-
|
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
|
-
|
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
|
|
@@ -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
|
-
|
36
|
+
inputSchema = schema["properties"]["input"]
|
37
37
|
|
38
38
|
# If input has a $ref, resolve it
|
39
|
-
if "$ref" in
|
40
|
-
ref =
|
41
|
-
|
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
|
54
|
-
for prop_name, prop in
|
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
|
69
|
-
input_prop_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
|
80
|
-
for prop_name, prop in
|
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
|
95
|
-
strict_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,94 +0,0 @@
|
|
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)
|
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
|
File without changes
|