Summary
A single MCP tool whose input schema uses standard JSON Schema annotation keywords (readOnly, x-google-enum-descriptions, …) makes the Gemini Realtime plugin fail to build its connect config, which kills the realtime session permanently — the agent joins the room, RoomIO links to the participant, and then neither hears nor speaks. No retry, no fallback, nothing visible to the end user.
Versions
livekit-agents 1.8.2 (= latest on PyPI)
livekit-plugins-google 1.8.2 (= latest on PyPI)
google-genai 2.18.1, pydantic 2.13.4
- model:
gemini-3.1-flash-live-preview (Gemini Realtime)
Also reproducible/valid by reading main today: _build_connect_config() is still called outside the try (line 971 vs 973) and _GeminiJsonSchema._simplify() still has no unknown-key handling (line 151+).
What happens
ERROR livekit.plugins.google Error in _main_task
pydantic_core._pydantic_core.ValidationError: 1 validation error for FunctionDeclaration
parameters.properties.attachments.items.properties.id.readOnly
Extra inputs are not permitted [type=extra_forbidden, input_value=True, input_type=bool]
The tools came from Google's own hosted Gmail MCP server (https://gmailmcp.googleapis.com/mcp/v1), attached through mcp_servers= on AgentSession. 4 of the 5 tools I allow-listed failed the same way:
| tool |
offending keyword |
create_draft |
readOnly: true on $defs.Attachment.properties.id |
get_thread, get_message |
x-google-enum-descriptions on messageFormat |
search_threads |
x-google-enum-descriptions on view |
Both keywords are legitimate JSON Schema (readOnly is spec; x-google-enum-descriptions is a Google extension), so any MCP server can produce them — this is not Gmail-specific.
Root cause (two defects)
-
_GeminiJsonSchema.simplify() doesn't drop unknown keywords. realtime_api._build_connect_config() calls create_tools_config(..., use_parameters_json_schema=False), so raw tools (which is what MCP tools are — RawFunctionTool) go through simplify(). It pops title/default/additionalProperties/$schema/const/discriminator/examples and resolves $ref, but anything else survives straight into types.FunctionDeclaration.model_validate(), whose Schema model is extra="forbid".
-
The config is built outside the try in _main_task. The exception escapes the while loop before any connection attempt, so there is no retry and no session: the worker sits in the room, publishes mic, and stays silent forever. A tool-schema problem should degrade the tool, not silence the agent.
Minimal repro (no LiveKit needed)
from google.genai import types
from livekit.plugins.google.utils import _GeminiJsonSchema
# exactly what the Gmail MCP returns for search_threads.view
schema = {
"type": "object",
"properties": {
"view": {
"type": "string",
"enum": ["THREAD_VIEW_UNSPECIFIED", "THREAD_VIEW_MINIMAL"],
"x-google-enum-descriptions": ["a", "b"],
}
},
}
params = _GeminiJsonSchema(schema).simplify() # keeps x-google-enum-descriptions
types.FunctionDeclaration.model_validate(
{"name": "t", "description": "", "parameters": params}
)
# pydantic_core._pydantic_core.ValidationError: Extra inputs are not permitted ...
For readOnly the same thing happens one level deeper, inside a $defs entry reached through $ref:
schema = {
"type": "object",
"properties": {
"attachments": {
"type": "array",
"items": {"$ref": "#/$defs/Attachment"},
}
},
"$defs": {
"Attachment": {
"type": "object",
"properties": {"id": {"type": "string", "readOnly": True}},
}
},
}
Suggested fix
- In
_GeminiJsonSchema._simplify, drop keywords the Gemini Schema doesn't know (its field set is a ready-made allowlist: types.Schema.model_fields) — these are annotations and carry no meaning for the model.
- In
realtime_api, build the connect config inside the try (or catch per-session and log), so a bad tool can't take the audio down with it. Dropping the offending declaration with a logger.warning would be even better than dropping the session.
I can open a PR for both if you'd like, but I wanted to file the report first since it's a session-level failure that's invisible from the client side.
Context
Friday (a Brazilian Portuguese voice assistant built on LiveKit Agents) went completely deaf right after the owner enabled the Gmail MCP in its capability list — 6 start attempts, all dead, with the only clue buried in the worker log. Workaround on my side was sanitizing MCP tool schemas before the plugin sees them; the underlying defects are the two above.
Summary
A single MCP tool whose input schema uses standard JSON Schema annotation keywords (
readOnly,x-google-enum-descriptions, …) makes the Gemini Realtime plugin fail to build its connect config, which kills the realtime session permanently — the agent joins the room,RoomIOlinks to the participant, and then neither hears nor speaks. No retry, no fallback, nothing visible to the end user.Versions
livekit-agents1.8.2 (= latest on PyPI)livekit-plugins-google1.8.2 (= latest on PyPI)google-genai2.18.1,pydantic2.13.4gemini-3.1-flash-live-preview(Gemini Realtime)Also reproducible/valid by reading
maintoday:_build_connect_config()is still called outside thetry(line 971 vs 973) and_GeminiJsonSchema._simplify()still has no unknown-key handling (line 151+).What happens
The tools came from Google's own hosted Gmail MCP server (
https://gmailmcp.googleapis.com/mcp/v1), attached throughmcp_servers=onAgentSession. 4 of the 5 tools I allow-listed failed the same way:create_draftreadOnly: trueon$defs.Attachment.properties.idget_thread,get_messagex-google-enum-descriptionsonmessageFormatsearch_threadsx-google-enum-descriptionsonviewBoth keywords are legitimate JSON Schema (
readOnlyis spec;x-google-enum-descriptionsis a Google extension), so any MCP server can produce them — this is not Gmail-specific.Root cause (two defects)
_GeminiJsonSchema.simplify()doesn't drop unknown keywords.realtime_api._build_connect_config()callscreate_tools_config(..., use_parameters_json_schema=False), so raw tools (which is what MCP tools are —RawFunctionTool) go throughsimplify(). It popstitle/default/additionalProperties/$schema/const/discriminator/examplesand resolves$ref, but anything else survives straight intotypes.FunctionDeclaration.model_validate(), whoseSchemamodel isextra="forbid".The config is built outside the
tryin_main_task. The exception escapes thewhileloop before any connection attempt, so there is no retry and no session: the worker sits in the room, publishes mic, and stays silent forever. A tool-schema problem should degrade the tool, not silence the agent.Minimal repro (no LiveKit needed)
For
readOnlythe same thing happens one level deeper, inside a$defsentry reached through$ref:Suggested fix
_GeminiJsonSchema._simplify, drop keywords the GeminiSchemadoesn't know (its field set is a ready-made allowlist:types.Schema.model_fields) — these are annotations and carry no meaning for the model.realtime_api, build the connect config inside thetry(or catch per-session and log), so a bad tool can't take the audio down with it. Dropping the offending declaration with alogger.warningwould be even better than dropping the session.I can open a PR for both if you'd like, but I wanted to file the report first since it's a session-level failure that's invisible from the client side.
Context
Friday (a Brazilian Portuguese voice assistant built on LiveKit Agents) went completely deaf right after the owner enabled the Gmail MCP in its capability list — 6 start attempts, all dead, with the only clue buried in the worker log. Workaround on my side was sanitizing MCP tool schemas before the plugin sees them; the underlying defects are the two above.