mirror of
https://github.com/open-webui/mcpo
synced 2025-06-26 18:26:58 +00:00
Merge pull request #52 from AlvinNorin/feat/mcp-parameterless-tools
feat: Add spec-compliant support for parameterless MCP tools
This commit is contained in:
@@ -8,6 +8,8 @@ from fastapi import FastAPI, Body, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from mcp import ClientSession, StdioServerParameters, types
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.types import CallToolResult
|
||||
|
||||
from mcpo.utils.auth import get_verify_api_key
|
||||
from pydantic import create_model
|
||||
from starlette.routing import Mount
|
||||
@@ -30,6 +32,25 @@ def get_python_type(param_type: str):
|
||||
return str # Fallback
|
||||
# Expand as needed. PRs welcome!
|
||||
|
||||
def process_tool_response(result: CallToolResult) -> list:
|
||||
"""Universal response processor for all tool endpoints"""
|
||||
response = []
|
||||
for content in result.content:
|
||||
if isinstance(content, types.TextContent):
|
||||
text = content.text
|
||||
if isinstance(text, str):
|
||||
try:
|
||||
text = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
response.append(text)
|
||||
elif isinstance(content, types.ImageContent):
|
||||
image_data = f"data:{content.mimeType};base64,{content.data}"
|
||||
response.append(image_data)
|
||||
elif isinstance(content, types.EmbeddedResource):
|
||||
# TODO: Handle embedded resources
|
||||
response.append("Embedded resource not supported yet.")
|
||||
return response
|
||||
|
||||
async def create_dynamic_endpoints(app: FastAPI, api_dependency=None):
|
||||
session = app.state.session
|
||||
@@ -40,7 +61,9 @@ async def create_dynamic_endpoints(app: FastAPI, api_dependency=None):
|
||||
server_info = getattr(result, "serverInfo", None)
|
||||
if server_info:
|
||||
app.title = server_info.name or app.title
|
||||
app.description = app.description
|
||||
app.description = (
|
||||
f"{server_info.name} MCP Server" if server_info.name else app.description
|
||||
)
|
||||
app.version = server_info.version or app.version
|
||||
|
||||
tools_result = await session.list_tools()
|
||||
@@ -51,10 +74,10 @@ async def create_dynamic_endpoints(app: FastAPI, api_dependency=None):
|
||||
endpoint_description = tool.description
|
||||
schema = tool.inputSchema
|
||||
|
||||
# Build Pydantic model
|
||||
model_fields = {}
|
||||
required_fields = schema.get("required", [])
|
||||
properties = schema.get("properties", {})
|
||||
|
||||
for param_name, param_schema in properties.items():
|
||||
param_type = param_schema.get("type", "string")
|
||||
param_desc = param_schema.get("description", "")
|
||||
@@ -65,43 +88,32 @@ async def create_dynamic_endpoints(app: FastAPI, api_dependency=None):
|
||||
Body(default_value, description=param_desc),
|
||||
)
|
||||
|
||||
FormModel = create_model(f"{endpoint_name}_form_model", **model_fields)
|
||||
if model_fields:
|
||||
FormModel = create_model(f"{endpoint_name}_form_model", **model_fields)
|
||||
|
||||
def make_endpoint_func(endpoint_name: str, FormModel, session: ClientSession):
|
||||
async def tool_endpoint(form_data: FormModel):
|
||||
args = form_data.model_dump(exclude_none=True)
|
||||
print(f"Calling {endpoint_name} with arguments:", args)
|
||||
result = await session.call_tool(endpoint_name, arguments=args)
|
||||
response = []
|
||||
for content in result.content:
|
||||
if isinstance(content, types.TextContent):
|
||||
text = content.text
|
||||
if isinstance(text, str):
|
||||
try:
|
||||
text = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
response.append(text)
|
||||
elif isinstance(content, types.ImageContent):
|
||||
image_data = content.data
|
||||
image_data = f"data:{content.mimeType};base64,{image_data}"
|
||||
response.append(image_data)
|
||||
elif isinstance(content, types.EmbeddedResource):
|
||||
# TODO: Handle embedded resources
|
||||
response.append("Embedded resource not supported yet.")
|
||||
def make_endpoint_func(endpoint_name: str, FormModel, session: ClientSession): # Parameterized endpoint
|
||||
async def tool_endpoint(form_data: FormModel):
|
||||
args = form_data.model_dump(exclude_none=True)
|
||||
result = await session.call_tool(endpoint_name, arguments=args)
|
||||
return process_tool_response(result)
|
||||
return tool_endpoint
|
||||
|
||||
return response
|
||||
tool_handler = make_endpoint_func(endpoint_name, FormModel, session)
|
||||
else:
|
||||
def make_endpoint_func_no_args(endpoint_name: str, session: ClientSession): # Parameterless endpoint
|
||||
async def tool_endpoint(): # No parameters
|
||||
result = await session.call_tool(endpoint_name, arguments={}) # Empty dict
|
||||
return process_tool_response(result) # Same processor
|
||||
return tool_endpoint
|
||||
|
||||
return tool_endpoint
|
||||
|
||||
tool = make_endpoint_func(endpoint_name, FormModel, session)
|
||||
tool_handler = make_endpoint_func_no_args(endpoint_name, session)
|
||||
|
||||
app.post(
|
||||
f"/{endpoint_name}",
|
||||
summary=endpoint_name.replace("_", " ").title(),
|
||||
description=endpoint_description,
|
||||
dependencies=[Depends(api_dependency)] if api_dependency else [],
|
||||
)(tool)
|
||||
)(tool_handler)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
||||
Reference in New Issue
Block a user