Compare commits

...
4 Commits
Author SHA1 Message Date
shadowdaoandClaude dc95e5ac55 Fix MCP tool execution to use proper JSON-RPC 2.0 format
OpenWebUI Discord Bot / Build-and-Push (push) Successful in 53s
The /mcp/call_tool endpoint expects JSON-RPC 2.0 format requests.
Updated to send proper RPC structure and parse RPC responses.

Request format:
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "tool_name",
    "arguments": {...}
  },
  "id": 1
}

Response parsing updated to extract result from JSON-RPC envelope:
result.result.content[0].text

This fixes the 400 validation error:
"Field required: JSONRPCRequest.method, jsonrpc, id"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 11:44:48 -08:00
shadowdaoandClaude 4f7b48c03b Fix 406 error in MCP tool execution - add Accept header
OpenWebUI Discord Bot / Build-and-Push (push) Successful in 53s
The /mcp/call_tool endpoint was returning 406 "Not Acceptable" error
because the request didn't include an Accept header.

Fixed by adding "Accept": "application/json" to the request headers.

Error message was:
"Not Acceptable: Client must accept application/json"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 11:41:07 -08:00
shadowdaoandClaude 94651b6ec1 Rewrite to use chat.completions with manual MCP tool execution
OpenWebUI Discord Bot / Build-and-Push (push) Successful in 55s
Major refactor to fix Bedrock + MCP compatibility issues:

- Removed Responses API approach (doesn't work with Bedrock)
- Added execute_mcp_tool() to manually call tools via /mcp/call_tool
- Rewrote get_available_mcp_tools() to return OpenAI function format
- Implemented manual tool execution loop with max 5 iterations
- Tool results are sent back to model in standard tool response format
- Removed query_needs_tools() function (no longer needed)

How it works:
1. Fetch MCP tools from /v1/mcp/tools and convert to OpenAI format
2. Call chat.completions.create() with tools array
3. When model requests tool calls, execute via POST /mcp/call_tool
4. Send results back to model with role="tool"
5. Loop until model provides final response

This bypasses the broken Responses API and uses working endpoints
that are compatible with AWS Bedrock + LiteLLM MCP integration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 11:34:24 -08:00
shadowdaoandClaude aca70dbd0b Add intelligent tool_choice parameter for MCP tools
OpenWebUI Discord Bot / Build-and-Push (push) Successful in 54s
Implements smart tool selection based on query content:
- Adds query_needs_tools() function to detect tool-requiring queries
- Sets tool_choice="required" for queries needing GitHub/time/weather/search
- Sets tool_choice="auto" for general conversation
- Adds debug logging for tool choice decisions

This fixes the issue where MCP tools were configured but not being used
because tool_choice defaulted to "auto" and the model opted not to use them.

Query detection keywords include:
- Time/date operations (time, clock, date, now, current)
- Weather queries (weather, temperature, forecast)
- GitHub operations (repo, code, file, commit, PR, issue)
- Search/lookup operations (search, find, get, fetch, retrieve)
- File operations (read, open, check, list, contents)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 11:08:09 -08:00
+201 -129
View File
@@ -80,37 +80,120 @@ async def download_image(url: str) -> str | None:
print(f"Error downloading image from {url}: {e}") print(f"Error downloading image from {url}: {e}")
return None return None
async def get_available_mcp_servers(): async def execute_mcp_tool(tool_name: str, arguments: dict) -> str:
"""Query LiteLLM for available MCP servers (used with Responses API)""" """Execute an MCP tool via LiteLLM's /mcp/call_tool endpoint"""
import json
try: try:
base_url = LITELLM_API_BASE.rstrip('/') base_url = LITELLM_API_BASE.rstrip('/')
headers = {"x-litellm-api-key": LITELLM_API_KEY} headers = {
"Authorization": f"Bearer {LITELLM_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json"
}
debug_log(f"Executing MCP tool: {tool_name} with args: {arguments}")
async with httpx.AsyncClient(timeout=60.0) as http_client:
response = await http_client.post(
f"{base_url}/mcp/call_tool",
headers=headers,
json={
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments
},
"id": 1
}
)
debug_log(f"MCP call_tool response status: {response.status_code}")
if response.status_code == 200:
result = response.json()
debug_log(f"MCP tool result: {str(result)[:200]}...")
# Handle JSON-RPC response format
if isinstance(result, dict):
# Check for JSON-RPC result
if "result" in result:
rpc_result = result["result"]
# MCP tool results have a "content" array
if isinstance(rpc_result, dict) and "content" in rpc_result:
content = rpc_result["content"]
if isinstance(content, list) and len(content) > 0:
# Handle text content blocks
first_content = content[0]
if isinstance(first_content, dict) and "text" in first_content:
return first_content["text"]
return json.dumps(content)
return json.dumps(content) if content else "Tool executed successfully"
return json.dumps(rpc_result)
# Fallback for non-RPC format
if "content" in result:
content = result["content"]
if isinstance(content, list) and len(content) > 0:
first_content = content[0]
if isinstance(first_content, dict) and "text" in first_content:
return first_content["text"]
return json.dumps(content)
return json.dumps(content) if content else "Tool executed successfully"
return json.dumps(result)
return str(result)
else:
error_text = response.text
debug_log(f"MCP call_tool error: {response.status_code} - {error_text}")
return f"Error executing tool: {response.status_code} - {error_text}"
except Exception as e:
debug_log(f"Exception calling MCP tool: {e}")
import traceback
debug_log(f"Traceback: {traceback.format_exc()}")
return f"Error executing tool: {str(e)}"
async def get_available_mcp_tools():
"""Query LiteLLM for available MCP tools and convert to OpenAI function format"""
try:
base_url = LITELLM_API_BASE.rstrip('/')
headers = {"Authorization": f"Bearer {LITELLM_API_KEY}"}
async with httpx.AsyncClient(timeout=30.0) as http_client: async with httpx.AsyncClient(timeout=30.0) as http_client:
# Get MCP server configuration # Get available MCP tools
server_response = await http_client.get( tools_response = await http_client.get(
f"{base_url}/v1/mcp/server", f"{base_url}/v1/mcp/tools",
headers=headers headers=headers
) )
if server_response.status_code == 200: if tools_response.status_code == 200:
server_info = server_response.json() tools_data = tools_response.json()
server_count = len(server_info) if isinstance(server_info, list) else 0 mcp_tools = tools_data.get("tools", []) if isinstance(tools_data, dict) else tools_data
debug_log(f"MCP server info: found {server_count} servers") debug_log(f"Found {len(mcp_tools)} MCP tools")
if server_count > 0: # Convert MCP tools to OpenAI function calling format
# Log server names for visibility openai_tools = []
server_names = [s.get("server_name") for s in server_info if isinstance(s, dict) and s.get("server_name")] for tool in mcp_tools:
debug_log(f"Available MCP servers: {server_names}") if isinstance(tool, dict) and tool.get("name") and tool.get("description"):
openai_tool = {
"type": "function",
"function": {
"name": tool["name"],
"description": tool.get("description", ""),
"parameters": tool.get("inputSchema", {"type": "object", "properties": {}})
}
}
openai_tools.append(openai_tool)
return {"server": server_info} debug_log(f"Converted {len(openai_tools)} tools to OpenAI format")
return openai_tools
else: else:
debug_log(f"MCP server endpoint returned {server_response.status_code}: {server_response.text}") debug_log(f"MCP tools endpoint returned {tools_response.status_code}")
except Exception as e: except Exception as e:
debug_log(f"Error fetching MCP servers: {e}") debug_log(f"Error fetching MCP tools: {e}")
return None return []
async def get_chat_history(channel, bot_user_id: int, limit: int = 50) -> List[Dict[str, Any]]: async def get_chat_history(channel, bot_user_id: int, limit: int = 50) -> List[Dict[str, Any]]:
""" """
@@ -256,7 +339,9 @@ async def get_chat_history(channel, bot_user_id: int, limit: int = 50) -> List[D
async def get_ai_response(history_messages: List[Dict[str, Any]], user_message: str, image_urls: List[str] = None) -> str: async def get_ai_response(history_messages: List[Dict[str, Any]], user_message: str, image_urls: List[str] = None) -> str:
""" """
Get AI response using LiteLLM Responses API with automatic MCP tool execution. Get AI response using LiteLLM chat.completions with manual MCP tool execution.
Uses manual tool execution loop since Responses API doesn't work with Bedrock + MCP.
Args: Args:
history_messages: List of previous conversation messages with roles history_messages: List of previous conversation messages with roles
@@ -266,128 +351,115 @@ async def get_ai_response(history_messages: List[Dict[str, Any]], user_message:
Returns: Returns:
AI response string AI response string
""" """
import json
# Build messages array
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(history_messages)
# Build current user message
if image_urls:
content_parts = [{"type": "text", "text": user_message}]
for url in image_urls:
base64_image = await download_image(url)
if base64_image:
content_parts.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
})
messages.append({"role": "user", "content": content_parts})
else:
messages.append({"role": "user", "content": user_message})
try: try:
# When tools are enabled, use Responses API with MCP for automatic tool execution # Build request parameters
request_params = {
"model": MODEL_NAME,
"messages": messages,
"temperature": 0.7,
}
# Add MCP tools if enabled
tools = []
if ENABLE_TOOLS: if ENABLE_TOOLS:
debug_log("Tools enabled - using Responses API with MCP auto-execution") debug_log("Tools enabled - fetching MCP tools")
tools = await get_available_mcp_tools()
# Query MCP server info to get server_label if tools:
mcp_info = await get_available_mcp_servers() request_params["tools"] = tools
request_params["tool_choice"] = "auto"
debug_log(f"Added {len(tools)} tools to request")
# Build input array with system prompt, history, and current message debug_log(f"Calling chat.completions with {len(tools)} tools")
input_messages = [] response = client.chat.completions.create(**request_params)
# Add system prompt as developer role for newer models # Handle tool calls if present
input_messages.append({ response_message = response.choices[0].message
"role": "developer", tool_calls = getattr(response_message, 'tool_calls', None)
"content": SYSTEM_PROMPT
# Tool execution loop (max 5 iterations to prevent infinite loops)
max_iterations = 5
iteration = 0
while tool_calls and len(tool_calls) > 0 and iteration < max_iterations:
iteration += 1
debug_log(f"Tool call iteration {iteration}: Model requested {len(tool_calls)} tool calls")
# Add assistant's response with tool calls to messages
messages.append({
"role": "assistant",
"content": response_message.content,
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
}
for tc in tool_calls
]
}) })
# Add conversation history # Execute each tool call via MCP
for msg in history_messages: for tool_call in tool_calls:
input_messages.append({ function_name = tool_call.function.name
"role": msg["role"], function_args_str = tool_call.function.arguments
"content": msg["content"]
debug_log(f"Executing tool: {function_name}")
# Parse arguments
try:
args_dict = json.loads(function_args_str) if isinstance(function_args_str, str) else function_args_str
except json.JSONDecodeError:
args_dict = {}
debug_log(f"Failed to parse tool arguments: {function_args_str}")
# Execute the tool via MCP
tool_result = await execute_mcp_tool(function_name, args_dict)
# Add tool result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result
}) })
# Build current user message # Get next response from model
if image_urls: debug_log("Getting model response after tool execution")
# Multi-modal message with text and images request_params["messages"] = messages
content_parts = [{"type": "text", "text": user_message}] response = client.chat.completions.create(**request_params)
for url in image_urls:
base64_image = await download_image(url)
if base64_image:
content_parts.append({
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
})
input_messages.append({"role": "user", "content": content_parts})
else:
input_messages.append({"role": "user", "content": user_message})
# Build MCP tools configuration response_message = response.choices[0].message
tools_config = [] tool_calls = getattr(response_message, 'tool_calls', None)
if mcp_info and isinstance(mcp_info, dict):
server_list = mcp_info.get("server", [])
if isinstance(server_list, list) and len(server_list) > 0:
for server_info in server_list:
server_name = server_info.get("server_name")
if server_name:
tools_config.append({
"type": "mcp",
"server_label": server_name,
"server_url": "litellm_proxy", # Use LiteLLM as MCP gateway
"require_approval": "never" # Automatic tool execution
})
debug_log(f"Added MCP server '{server_name}' with auto-execution")
if not tools_config: if iteration >= max_iterations:
debug_log("No MCP servers found, falling back to standard chat completions") debug_log(f"Warning: Reached max tool iterations ({max_iterations})")
# Fall through to standard chat completions below
else:
# Use Responses API with MCP tools
debug_log(f"Calling Responses API with {len(tools_config)} MCP servers")
debug_log(f"Input messages: {len(input_messages)} messages")
response = client.responses.create( final_content = response.choices[0].message.content
model=MODEL_NAME, debug_log(f"Final response: {final_content[:100] if final_content else 'None'}...")
input=input_messages, return final_content or "I received a response but it was empty. Please try again."
tools=tools_config,
stream=False
)
debug_log(f"Response received, status: {getattr(response, 'status', 'unknown')}")
# Extract text from Responses API format
# Try the shorthand first
response_text = getattr(response, 'output_text', None)
if response_text:
debug_log(f"Got response via output_text shorthand: {response_text[:100]}...")
return response_text
# Otherwise navigate the structure
if hasattr(response, 'output') and len(response.output) > 0:
for output in response.output:
if hasattr(output, 'type') and output.type == "message":
if hasattr(output, 'content') and len(output.content) > 0:
for content in output.content:
if hasattr(content, 'type') and content.type == "output_text":
debug_log(f"Got response via structure navigation: {content.text[:100]}...")
return content.text
debug_log(f"Unexpected response format: {response}")
debug_log(f"Response attributes: {dir(response)}")
return "I received a response but couldn't extract the text. Please try again."
# Standard chat completions (when tools disabled or MCP not available)
debug_log("Using standard chat completions")
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(history_messages)
if image_urls:
content_parts = [{"type": "text", "text": user_message}]
for url in image_urls:
base64_image = await download_image(url)
if base64_image:
content_parts.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
})
messages.append({"role": "user", "content": content_parts})
else:
messages.append({"role": "user", "content": user_message})
response = client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
temperature=0.7
)
return response.choices[0].message.content
except Exception as e: except Exception as e:
error_msg = f"Error calling LiteLLM API: {str(e)}" error_msg = f"Error calling LiteLLM API: {str(e)}"