diff --git a/examples/foundational/39c-mcp-run-http.py b/examples/foundational/39c-mcp-run-http.py index 1c51894b1..6c39da75c 100644 --- a/examples/foundational/39c-mcp-run-http.py +++ b/examples/foundational/39c-mcp-run-http.py @@ -18,8 +18,8 @@ from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.google.llm import GoogleLLMService from pipecat.services.mcp_service import MCPClient -from pipecat.services.openai.llm import OpenAILLMService from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams from pipecat.transports.services.daily import DailyParams @@ -58,7 +58,7 @@ async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_si voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady ) - llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini") + llm = GoogleLLMService(api_key=os.getenv("GOOGLE_API_KEY"), model="gemini-2.0-flash") try: # Github MCP docs: https://github.com/github/github-mcp-server diff --git a/src/pipecat/services/gemini_multimodal_live/gemini.py b/src/pipecat/services/gemini_multimodal_live/gemini.py index a31bbfe70..152bad215 100644 --- a/src/pipecat/services/gemini_multimodal_live/gemini.py +++ b/src/pipecat/services/gemini_multimodal_live/gemini.py @@ -583,6 +583,17 @@ class GeminiMultimodalLiveLLMService(LLMService): """ return True + def needs_mcp_alternate_schema(self) -> bool: + """Check if this LLM service requires alternate MCP schema. + + Google/Gemini has stricter JSON schema validation and requires + certain properties to be removed or modified for compatibility. + + Returns: + True for Google/Gemini services. + """ + return True + def set_audio_input_paused(self, paused: bool): """Set the audio input pause state. diff --git a/src/pipecat/services/google/llm.py b/src/pipecat/services/google/llm.py index b4ddb3c00..bc7595382 100644 --- a/src/pipecat/services/google/llm.py +++ b/src/pipecat/services/google/llm.py @@ -717,6 +717,17 @@ class GoogleLLMService(LLMService): """ return True + def needs_mcp_alternate_schema(self) -> bool: + """Check if this LLM service requires alternate MCP schema. + + Google/Gemini has stricter JSON schema validation and requires + certain properties to be removed or modified for compatibility. + + Returns: + True for Google/Gemini services. + """ + return True + def _create_client(self, api_key: str): self._client = genai.Client(api_key=api_key) diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 51a5688f9..669a34803 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -355,6 +355,17 @@ class LLMService(AIService): return True return function_name in self._functions.keys() + def needs_mcp_alternate_schema(self) -> bool: + """Check if this LLM service requires alternate MCP schema. + + Some LLM services have stricter JSON schema validation and require + certain properties to be removed or modified for compatibility. + + Returns: + True if MCP schemas should be cleaned for this service, False otherwise. + """ + return False + async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]): """Execute a sequence of function calls from the LLM. diff --git a/src/pipecat/services/mcp_service.py b/src/pipecat/services/mcp_service.py index 31a29354b..69d79844c 100644 --- a/src/pipecat/services/mcp_service.py +++ b/src/pipecat/services/mcp_service.py @@ -53,6 +53,7 @@ class MCPClient(BaseObject): super().__init__(**kwargs) self._server_params = server_params self._session = ClientSession + self._needs_alternate_schema = False if isinstance(server_params, StdioServerParameters): self._client = stdio_client @@ -80,9 +81,48 @@ class MCPClient(BaseObject): Returns: A ToolsSchema containing all successfully registered tools. """ + # Check once if the LLM needs alternate strict schema + self._needs_alternate_schema = llm and llm.needs_mcp_alternate_schema() tools_schema = await self._register_tools(llm) return tools_schema + def _get_alternate_schema_for_strict_validation(self, schema: Dict[str, Any]) -> Dict[str, Any]: + """Get an alternate JSON schema to be compatible with LLMs that have strict validation. + + Some LLMs have stricter validation and don't allow certain schema properties + that are valid in standard JSON Schema. + + Args: + schema: The JSON schema to get an alternate schema for + + Returns: + An alternate schema compatible with strict validation + """ + if not isinstance(schema, dict): + return schema + + alternate_schema = {} + + for key, value in schema.items(): + # Skip additionalProperties as some LLMs don't like additionalProperties: false + if key == "additionalProperties": + continue + + # Recursively get alternate schema for nested objects + if isinstance(value, dict): + alternate_schema[key] = self._get_alternate_schema_for_strict_validation(value) + elif isinstance(value, list): + alternate_schema[key] = [ + self._get_alternate_schema_for_strict_validation(item) + if isinstance(item, dict) + else item + for item in value + ] + else: + alternate_schema[key] = value + + return alternate_schema + def _convert_mcp_schema_to_pipecat( self, tool_name: str, tool_schema: Dict[str, Any] ) -> FunctionSchema: @@ -100,6 +140,11 @@ class MCPClient(BaseObject): properties = tool_schema["input_schema"].get("properties", {}) required = tool_schema["input_schema"].get("required", []) + # Only get alternate schema for LLMs that need strict schema validation + if self._needs_alternate_schema: + logger.debug("Getting alternate schema for strict validation") + properties = self._get_alternate_schema_for_strict_validation(properties) + schema = FunctionSchema( name=tool_name, description=tool_schema["description"], @@ -280,7 +325,8 @@ class MCPClient(BaseObject): try: # Convert the schema function_schema = self._convert_mcp_schema_to_pipecat( - tool_name, {"description": tool.description, "input_schema": tool.inputSchema} + tool_name, + {"description": tool.description, "input_schema": tool.inputSchema}, ) # Register the wrapped function