Skip to content

Add external prompt data and live MCP support - #30

Closed
Lazare-42 wants to merge 5 commits into
mainfrom
feature/external-data-mcp-fast-speech
Closed

Lazare-42 wants to merge 5 commits into
mainfrom
feature/external-data-mcp-fast-speech

Conversation

@Lazare-42

Copy link
Copy Markdown
Contributor

Adds external prompt data sources, live remote MCP tool execution, speech speed controls, committed Speaking Bot OpenAPI snapshot, and security hardening for public MCP inputs/URL fetching.\n\nValidation:\n- nix develop -c poetry run python -m unittest tests.test_mcp_client tests.test_models tests.test_prompt_context tests.test_runtime tests.test_openapi_snapshot\n- nix develop -c ruff check app/models.py app/services/prompt_context.py utils/mcp_client.py scripts/meetingbaas.py tests/test_mcp_client.py tests/test_models.py tests/test_prompt_context.py tests/test_openapi_snapshot.py scripts/export_openapi.py

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 98038909-969f-417d-8b9f-1c874b5a1ba8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR adds external prompt-context loading and MCP server integration to the speaking bot API. It also updates persona handoff, Cartesia TTS speed configuration, OpenAPI snapshots, environment docs, and tests.

Changes

Prompt context and MCP API surface

Layer / File(s) Summary
Data models for prompt sources and MCP config
app/models.py
Adds prompt-source and MCP configuration models and extends BotRequest with prompt data, MCP, and speech speed fields.
Prompt context loading service
app/services/prompt_context.py, tests/test_prompt_context.py
Implements token budgeting, URL loading with SSRF checks, MCP context formatting, and merge helpers, with unit tests.
MCP client library
utils/mcp_client.py, tests/test_mcp_client.py, tests/test_models.py
Adds stdio and HTTP MCP clients, request normalization, URL validation, and transport/model validation tests.
Route wiring for prompt context and persona payload
app/routes.py
Loads prompt context into persona data, adds MCP and speech speed fields downstream, and removes the old prompt-string assembly path.
Persona payload file handoff to Pipecat subprocess
core/process.py
Writes persona data to disk and passes it to the subprocess via --persona-data-file.
Pipecat runtime: TTS speed and live MCP tools
scripts/meetingbaas.py
Adds TTS speed resolution, live MCP tool registration, MCP prompt injection, persona file loading, and shutdown handling.
OpenAPI snapshot generation and env/docs
scripts/export_openapi.py, speaking-bot-openapi.json, tests/test_openapi_snapshot.py, env.example, README.md
Adds the exported OpenAPI snapshot, documents the new request/env surface, and validates the committed schema snapshots.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Routes as app/routes.py
  participant PromptContext as app/services/prompt_context.py
  participant Process as core/process.py
  participant Meetingbaas as scripts/meetingbaas.py
  participant MCPServer as MCP server

  Client->>Routes: POST /bots (prompt_data_sources, mcp, speech_speed)
  Routes->>PromptContext: load_prompt_context(sources, token_limit)
  PromptContext-->>Routes: PromptContextResult(block, sources, estimated_tokens)
  Routes->>Process: persona_data with context, mcp, speech_speed
  Process->>Meetingbaas: spawn subprocess --persona-data-file path
  Meetingbaas->>MCPServer: connect and discover tools
  MCPServer-->>Meetingbaas: tools and metadata
  Meetingbaas-->>Client: meeting bot starts with configured prompt/tools/TTS
Loading

Possibly related PRs

Suggested reviewers: cyril-l-m

Poem

A rabbit hopped with prompt bags full,
MCP tools chattered, swift and cool.
TTS sped on with tidy grace,
And snapshots lined the docs in place.
Hop hop — the bot now knows the tune!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly captures the main changes: external prompt data and live MCP support.
Description check ✅ Passed The description accurately summarizes the new prompt data, MCP, speech speed, OpenAPI snapshot, and hardening changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/external-data-mcp-fast-speech

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models.py`:
- Around line 4-6: Update the new Pydantic models in models.py to use modern
built-in generics and union syntax instead of typing.Dict, typing.List, and
Optional. Replace these annotations throughout the affected model definitions
and related validators with dict, list, and | None so the new fields and models
match the project guidelines and avoid Ruff UP035 warnings. Focus on the model
classes and any helper methods that currently reference the old typing aliases.

In `@app/routes.py`:
- Around line 250-251: The MCP persona persistence path is currently serializing
sensitive headers into resolved_persona_data via
request.mcp.model_dump(exclude_none=True), which later gets written to disk.
Update the logic in the request.mcp handling block to exclude servers[].headers
from the persisted MCP payload, keeping only non-sensitive MCP fields in
resolved_persona_data before it is saved.

In `@app/services/prompt_context.py`:
- Around line 135-172: The SSRF guard in _validate_fetch_url only checks the
hostname before the request is opened, so DNS rebinding can still reach a
private IP later. Update the fetch path that uses _validate_fetch_url to either
pin the resolved address in the aiohttp connector or re-validate the actual peer
IP after connect, and keep the existing private-host/address checks in place.

In `@core/process.py`:
- Around line 48-59: The temp payload written in core/process.py by the persona
payload setup before launching the Pipecat subprocess can be left behind if
Popen fails. Update the parent error handling around the subprocess startup path
so that persona_data_path is removed on failure, using the existing payload
creation flow near payload_dir, persona_data_path, and the Popen launch logic to
ensure the 0600 file does not persist with persona/MCP data.

In `@env.example`:
- Around line 69-70: The env example is missing the allowlist variable name used
elsewhere, and the current entry only documents MCP_ALLOW_PRIVATE_URLS. Update
the env.example entry to include MCP_ALLOWED_PRIVATE_URLS alongside the existing
private-URL setting, matching the names referenced by README.md and
utils/mcp_client.py so both supported env vars are documented consistently.

In `@scripts/export_openapi.py`:
- Around line 17-20: Add a Google-style docstring to main() in
export_openapi.py, since it is a public function and currently lacks one. Keep
the docstring concise but complete, describing that main() generates the OpenAPI
schema from create_app().openapi(), writes it to OUTPUT, and prints the written
path. Use the function name main() as the anchor when updating the script.

In `@scripts/meetingbaas.py`:
- Around line 4-6: The module is importing the same JSON library under two
names, which creates unnecessary aliasing and confusion. Update the imports in
meetingbaas.py to use the existing json module name everywhere, and adjust
_tool_result_to_text to reference json instead of jsonlib while keeping cli()
and other JSON usages consistent with the single import.
- Around line 276-320: The connect() flow in MeetingBaaS leaks already-open MCP
clients when a later required server fails, because the bare re-raise exits
before cleanup. Update connect() to close every client that was successfully
added to self._clients before propagating the error, or ensure
setup_mcp_tools/main performs cleanup on connect() failure; use the connect,
self._clients, self._build_client, and log_and_flush symbols to locate the
affected path.

In `@utils/mcp_client.py`:
- Around line 384-395: The _request method in mcp_client.py currently assumes
the next framed stdout message is always the matching JSON-RPC response, which
can misattribute interleaved notifications or other replies. Update _request to
use the request id returned by self._state.request(method, params), then keep
reading from self._process.stdout with read_stdio_message until a frame with a
matching id is found, ignoring unrelated notifications or responses. Make sure
the final _extract_result call only runs on the response whose id matches the
original request.
- Around line 88-117: DNS rebinding can still slip past validate_mcp_http_url()
because it validates the hostname once, but _post() later hands the original URL
to aiohttp for a second resolution at connect time. Update the connection path
in _post() (and any related aiohttp session/connector setup) so the validated
address is pinned or enforced during connection, using the existing helpers
validate_mcp_http_url(), _is_private_ip(), and _private_mcp_urls_allowed() as
the reference points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9c152c2f-9468-47ff-ad7f-84db40d7eaf5

📥 Commits

Reviewing files that changed from the base of the PR and between 274ac1d and fc50ccc.

📒 Files selected for processing (14)
  • README.md
  • app/models.py
  • app/routes.py
  • app/services/prompt_context.py
  • core/process.py
  • env.example
  • scripts/export_openapi.py
  • scripts/meetingbaas.py
  • speaking-bot-openapi.json
  • tests/test_mcp_client.py
  • tests/test_models.py
  • tests/test_openapi_snapshot.py
  • tests/test_prompt_context.py
  • utils/mcp_client.py

Comment thread app/models.py
Comment on lines +4 to +6
from typing import Any, Dict, List, Literal, Optional

from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use modern type-hint syntax for the new models.

The new fields and models use typing.Dict, typing.List, and Optional[...] (e.g., Lines 67-84, 122-154, 262-276). Ruff (UP035) also flags Dict/List as deprecated. Prefer built-in generics (dict, list) and | None unions to match guidelines.

As per coding guidelines: "Use type hints with modern Python syntax (|) for unions instead of Union" and "Prefer built-in types for annotations where possible".

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 4-4: typing.Dict is deprecated, use dict instead

(UP035)


[warning] 4-4: typing.List is deprecated, use list instead

(UP035)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models.py` around lines 4 - 6, Update the new Pydantic models in
models.py to use modern built-in generics and union syntax instead of
typing.Dict, typing.List, and Optional. Replace these annotations throughout the
affected model definitions and related validators with dict, list, and | None so
the new fields and models match the project guidelines and avoid Ruff UP035
warnings. Focus on the model classes and any helper methods that currently
reference the old typing aliases.

Sources: Coding guidelines, Linters/SAST tools

Comment thread app/routes.py
Comment on lines +250 to +251
if request.mcp:
resolved_persona_data["mcp"] = request.mcp.model_dump(exclude_none=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how persona_data is written to disk and its permissions/cleanup
fd 'process.py' core | xargs -I{} sed -n '1,200p' {}
rg -nP 'persona[-_]data[-_]file|persona_data|chmod|0o600|NamedTemporaryFile|unlink|os\.remove' -C3

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 5237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== app/routes.py around the referenced lines =="
sed -n '220,270p' app/routes.py

echo
echo "== Locate MCP-related schema/model definitions =="
rg -n "class .*MCP|mcp:|headers|Authorization|model_dump|servers" app -g '*.py' -C 3

echo
echo "== Find the subprocess handoff script and cleanup behavior =="
rg -n --hidden --glob '*.py' "persona_data_path|--persona-data-file|persona_payloads|unlink|os\.remove|rmtree|NamedTemporaryFile|chmod|0o600" .

echo
echo "== Inspect the process writer file if present =="
fd -a 'process.py' . | while read -r f; do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 20618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== app/services/prompt_context.py MCP formatter =="
sed -n '242,310p' app/services/prompt_context.py

echo
echo "== scripts/meetingbaas.py persona-data handling and cleanup =="
sed -n '1030,1085p' scripts/meetingbaas.py

echo
echo "== any direct logging of MCP headers or request.mcp dumps =="
rg -n "mcp.*header|headers.*mcp|model_dump\\(exclude_none=True\\).*mcp|persona_data_file|prompt_context.*mcp|format_mcp_context" app scripts core -C 2

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 5539


Strip MCP headers before persisting the persona payload (app/routes.py:250-251)
request.mcp.model_dump(exclude_none=True) copies servers[].headers into resolved_persona_data. That object is later written to persona_payloads/<client>.json, so bearer tokens can sit on disk briefly even with 0600 permissions and cleanup after startup. Keep headers out of the persisted persona object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/routes.py` around lines 250 - 251, The MCP persona persistence path is
currently serializing sensitive headers into resolved_persona_data via
request.mcp.model_dump(exclude_none=True), which later gets written to disk.
Update the logic in the request.mcp handling block to exclude servers[].headers
from the persisted MCP payload, keeping only non-sensitive MCP fields in
resolved_persona_data before it is saved.

Comment on lines +135 to +172
def _validate_fetch_url(url: str) -> None:
"""Block obvious SSRF targets unless explicitly allowed."""
parsed = urlparse(url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise PromptContextError(
f"Invalid prompt data source URL: {url}",
status_code=400,
)

if _private_urls_allowed():
return

host = parsed.hostname
try:
if _is_private_ip(host):
raise PromptContextError(
f"Prompt data source URL host is private or local: {host}",
status_code=400,
)
return
except ValueError:
pass

try:
addresses = socket.getaddrinfo(host, None)
except socket.gaierror as e:
raise PromptContextError(
f"Could not resolve prompt data source host '{host}': {e}",
status_code=400,
) from e

for address in addresses:
resolved_ip = address[4][0]
if _is_private_ip(resolved_ip):
raise PromptContextError(
f"Prompt data source URL resolves to private or local address: {host}",
status_code=400,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

aiohttp pin resolved IP prevent DNS rebinding SSRF custom resolver

💡 Result:

To prevent SSRF and DNS rebinding attacks in aiohttp, relying on standard hostname-based validation is insufficient due to the TOCTOU (time-of-check to time-of-use) gap inherent in DNS resolution [1][2]. Because standard DNS lookups and subsequent TCP connections occur as separate operations, an attacker can modify DNS records between the check and the connection to redirect traffic to unauthorized IP addresses (e.g., private ranges, metadata services) [3][2]. To effectively secure aiohttp, implement the following architectural patterns: 1. Use a Custom Resolver Implement a custom resolver that intercepts the resolution process [4][2]. This resolver should: - Resolve the hostname to an IP address [3][2]. - Perform a security check on the returned IP address before it is returned to the connector [1][2]. - Validate that the IP does not fall within restricted ranges (e.g., RFC 1918, link-local 169.254/16, loopback, or cloud metadata endpoints) [1][2]. - By integrating this into the aiohttp TCPConnector lifecycle, you ensure that only validated IP addresses are ever used for the connection [1][3]. 2. Pin the Resolved IP To close the TOCTOU window, ensure that your logic pins the connection to the specific, validated IP address [1][3]. - After the custom resolver validates an IP, the transport layer should be instructed to connect directly to that IP [1][3]. - Update the request headers (specifically the Host header) and TLS SNI parameters to match the original hostname while the connection is established to the validated IP [3]. This preserves application-level requirements (like virtual hosting) without re-initiating a potentially malicious DNS resolution [3]. 3. Validate Redirects DNS rebinding often occurs or is weaponized during HTTP redirects [3]. Ensure your implementation is not just validating the initial URL, but also hooking into the request lifecycle to re-validate every subsequent redirect target against the same blocklist/policy before allowing the connection [1][3]. 4. Mitigate Resolver Bypasses Ensure your code correctly handles and restricts IP literals. Recent security updates emphasize that non-canonical IP formats (e.g., certain legacy numeric IPv4 forms) should be strictly validated or rejected, as they can sometimes bypass standard resolution checks [5]. For a robust, ready-to-use approach, consider using security-focused wrapper libraries that implement these "resolve-validate-pin" patterns as a single atomic operation, as manually implementing secure resolvers requires careful handling of edge cases like IPv4-mapped IPv6 addresses and non-standard host strings [1][3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby helpers.
git ls-files app/services/prompt_context.py
wc -l app/services/prompt_context.py
sed -n '1,260p' app/services/prompt_context.py

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 8513


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find fetch and validation helpers referenced in the review comment.
rg -n "_validate_fetch_url|_fetch_url_source|private_urls_allowed|getaddrinfo|aiohttp" app/services/prompt_context.py

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 629


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Extract only the relevant section with line numbers for precise inspection.
nl -ba app/services/prompt_context.py | sed -n '120,240p'

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the fetch implementation and surrounding code paths.
nl -ba app/services/prompt_context.py | sed -n '1,260p'

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 211


🌐 Web query:

aiohttp ClientSession TCPConnector DNS resolution connect time custom resolver documentation

💡 Result:

To configure a custom DNS resolver and manage connection/DNS resolution times in aiohttp, you primarily use the TCPConnector class [1][2]. Custom DNS Resolver To use custom nameservers for DNS resolution, you must have the aiodns library installed [1][3]. You can then pass an AsyncResolver instance to the TCPConnector [1][4]: from aiohttp.resolver import AsyncResolver import aiohttp # Create a resolver with custom nameservers resolver = AsyncResolver(nameservers=["8.8.8.8", "8.8.4.4"]) # Pass the resolver to the TCPConnector connector = aiohttp.TCPConnector(resolver=resolver) session = aiohttp.ClientSession(connector=connector) Note: For best results in an asynchronous context, initialize your resolver within an async function or ensure it is created within the running event loop to avoid potential warnings or lifecycle issues [5]. Managing Resolution and Connection Time aiohttp does not have a single flag for "DNS resolution time" independent of the connection process; instead, DNS resolution is handled as the first step of the connection phase [6]. You can manage timeouts and resolution behavior using the following: 1. DNS Cache Tuning: By default, TCPConnector uses an internal DNS cache (TTL of 10 seconds) [1][2]. You can disable this or change the TTL to ensure more frequent resolutions: - Disable cache: TCPConnector(use_dns_cache=False) [1][7]. - Adjust TTL: TCPConnector(ttl_dns_cache=300) [1][8]. 2. Connection Timeouts: Use aiohttp.ClientTimeout to control the total time spent connecting (which includes DNS resolution) [6]. - timeout = aiohttp.ClientTimeout(connect=5.0) - session.get(url, timeout=timeout) If the DNS lookup hangs or takes too long, it will be subject to the connect timeout defined in your ClientTimeout configuration [6]. If you encounter issues with DNS errors being swallowed or timeouts, ensure you are on a recent version of aiohttp, as fixes for DNS resolution error handling have been actively addressed in recent releases [6].

Citations:


Close the DNS-rebinding gap in this SSRF check _validate_fetch_url only validates the hostname before aiohttp opens the connection, so a rebinding attacker can still swap in a private IP at connect time. Pin the resolved address in the connector or re-check the peer IP after connect.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 139-142: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 150-153: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 154-154: Consider moving this statement to an else block

(TRY300)


[warning] 161-164: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 169-172: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/prompt_context.py` around lines 135 - 172, The SSRF guard in
_validate_fetch_url only checks the hostname before the request is opened, so
DNS rebinding can still reach a private IP later. Update the fetch path that
uses _validate_fetch_url to either pin the resolved address in the aiohttp
connector or re-validate the actual peer IP after connect, and keep the existing
private-host/address checks in place.

Comment thread core/process.py
Comment on lines +48 to +59
payload_dir = os.path.join(get_state_dir(), "persona_payloads")
os.makedirs(payload_dir, exist_ok=True)
persona_data_path = os.path.join(payload_dir, f"{client_id}.json")
payload_fd = os.open(
persona_data_path,
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
0o600,
)
with os.fdopen(payload_fd, "w") as f:
json.dump(persona_data, f)
if (persona_data or {}).get("mcp"):
logger.info(f"Passing MCP metadata to Pipecat process for client {client_id}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'start_pipecat_process\s*\(' --type=py
rg -nP -C3 '\bclient_id\b\s*=' --type=py -g 'app/**'

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map repository files around the relevant areas.
git ls-files 'core/*' '*/process.py' '*/process/*.py' | sed -n '1,200p'

# Inspect the relevant file structure.
if [ -f core/process.py ]; then
  wc -l core/process.py
  ast-grep outline core/process.py --view expanded || true
  sed -n '1,220p' core/process.py | cat -n
fi

# Find all references to client_id and persona_data_file.
rg -n --hidden --glob '!**/.git/**' '\bclient_id\b|persona_data_file|persona_payloads|start_pipecat_process' .

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 24932


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '140,210p' app/websockets.py | cat -n
sed -n '370,420p' app/routes.py | cat -n
sed -n '1045,1070p' scripts/meetingbaas.py | cat -n

# Check whether any cleanup happens on start failure.
rg -n --hidden --glob '!**/.git/**' 'persona_data_path|os\.remove\(args\.persona_data_file\)|persona_payloads|start_pipecat_process\(' core app scripts utils

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 8612


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for validation/sanitization of client_id before it reaches core.process.
rg -n --hidden --glob '!**/.git/**' 'client_id.*(uuid|validate|sanitize|safe|basename|replace)|basename\(.*client_id|client_id.*basename|re\.match\(.*client_id|re\.fullmatch\(.*client_id' app core utils scripts

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the websocket flow that reaches start_pipecat_process.
sed -n '1,140p' app/websockets.py | cat -n
sed -n '140,220p' app/websockets.py | cat -n

# Show the route that creates bot_client_id.
sed -n '90,140p' app/routes.py | cat -n

# Check for any other start_pipecat_process call sites.
rg -n --hidden --glob '!**/.git/**' 'start_pipecat_process\(' app core scripts utils

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 14510


Delete the temp payload file if Popen fails. core/process.py:48-59 writes persona_data_path before the subprocess starts, so a startup exception leaves the 0600 payload on disk with persona/MCP data. Remove it in the parent error path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/process.py` around lines 48 - 59, The temp payload written in
core/process.py by the persona payload setup before launching the Pipecat
subprocess can be left behind if Popen fails. Update the parent error handling
around the subprocess startup path so that persona_data_path is removed on
failure, using the existing payload creation flow near payload_dir,
persona_data_path, and the Popen launch logic to ensure the 0600 file does not
persist with persona/MCP data.

Source: Linters/SAST tools

Comment thread env.example
Comment on lines +69 to +70
# Set true only in trusted networks if remote MCP URLs must reach localhost/private IPs.
MCP_ALLOW_PRIVATE_URLS=false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'MCP_ALLOWED_PRIVATE_URLS|MCP_ALLOW_PRIVATE_URLS' --type=py

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n-- README references --\n'
rg -n 'MCP_ALLOWED_PRIVATE_URLS|MCP_ALLOW_PRIVATE_URLS' README.md env.example || true

printf '\n-- All repository references --\n'
rg -n 'MCP_ALLOWED_PRIVATE_URLS|MCP_ALLOW_PRIVATE_URLS' . || true

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 1716


Document MCP_ALLOWED_PRIVATE_URLS in env.example. README.md and utils/mcp_client.py both support the allowlist env var, but only MCP_ALLOW_PRIVATE_URLS is listed here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env.example` around lines 69 - 70, The env example is missing the allowlist
variable name used elsewhere, and the current entry only documents
MCP_ALLOW_PRIVATE_URLS. Update the env.example entry to include
MCP_ALLOWED_PRIVATE_URLS alongside the existing private-URL setting, matching
the names referenced by README.md and utils/mcp_client.py so both supported env
vars are documented consistently.

Comment thread scripts/export_openapi.py Outdated
Comment on lines +17 to +20
def main() -> None:
schema = create_app().openapi()
OUTPUT.write_text(json.dumps(schema, indent=2) + "\n", encoding="utf-8")
print(f"Wrote {OUTPUT.relative_to(ROOT)}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding a docstring to main().

Per coding guidelines, public functions should have Google-style docstrings. main() currently has none.

As per coding guidelines, "Write comprehensive docstrings for public APIs following Google-style docstring format."

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 18-18: use jsonify instead of json.dumps for JSON output
Context: json.dumps(schema, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/export_openapi.py` around lines 17 - 20, Add a Google-style docstring
to main() in export_openapi.py, since it is a public function and currently
lacks one. Keep the docstring concise but complete, describing that main()
generates the OpenAPI schema from create_app().openapi(), writes it to OUTPUT,
and prints the written path. Use the function name main() as the anchor when
updating the script.

Source: Coding guidelines

Comment thread scripts/meetingbaas.py
Comment on lines +4 to +6
import inspect
import json as jsonlib
from dataclasses import dataclass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid importing json twice under two names.

The module already imports json (used in cli() at Lines 1058/1071); adding import json as jsonlib for _tool_result_to_text means two aliases for the same module. Consolidate on json to reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/meetingbaas.py` around lines 4 - 6, The module is importing the same
JSON library under two names, which creates unnecessary aliasing and confusion.
Update the imports in meetingbaas.py to use the existing json module name
everywhere, and adjust _tool_result_to_text to reference json instead of jsonlib
while keeping cli() and other JSON usages consistent with the single import.

Comment thread scripts/meetingbaas.py
Comment on lines +276 to +320
async def connect(self) -> list[dict]:
discovered = []
for server in self._mcp_config.get("servers") or []:
if not isinstance(server, dict) or server.get("enabled") is False:
continue
if not server.get("transport"):
continue

client = self._build_client(server)
server_name = str(server.get("name") or "mcp")
try:
await client.initialize()
self._clients.append(client)
tool_allowlist = set(server.get("tool_allowlist") or [])
for tool in await client.list_tools():
tool_name = str(tool.get("name") or "")
if not tool_name:
continue
if tool_allowlist and tool_name not in tool_allowlist:
continue
function_name = build_mcp_tool_name(server_name, tool_name)
tool_ref = LiveMCPTool(
function_name=function_name,
server_name=server_name,
tool_name=tool_name,
client=client,
schema=tool,
)
self._tools[function_name] = tool_ref
discovered.append(
{
**tool,
"server_name": server_name,
"function_name": function_name,
}
)
except Exception as exc:
await client.close()
if server.get("required"):
raise
log_and_flush(
logging.WARNING,
f"[MCP] Could not connect server {server_name}: {exc}",
)
return discovered

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Required-server failure leaks already-connected clients and aborts startup.

When an earlier server connects successfully (appended to self._clients) and a later required server raises, the raise on Line 315 propagates without closing the earlier clients. Because setup_mcp_tools(...) is called at Line 767 — before the try/finally at Line 982 — the exception escapes main entirely: mcp_manager is never assigned, the shutdown teardown at Lines 1001-1006 never runs, and the connected MCP sessions leak while the bot fails to start.

Close all opened clients before re-raising (or wrap connect() in setup_mcp_tools and close the manager on failure).

🔒️ Proposed fix in connect()
             except Exception as exc:
                 await client.close()
                 if server.get("required"):
+                    await self.close()
                     raise
                 log_and_flush(
                     logging.WARNING,
                     f"[MCP] Could not connect server {server_name}: {exc}",
                 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def connect(self) -> list[dict]:
discovered = []
for server in self._mcp_config.get("servers") or []:
if not isinstance(server, dict) or server.get("enabled") is False:
continue
if not server.get("transport"):
continue
client = self._build_client(server)
server_name = str(server.get("name") or "mcp")
try:
await client.initialize()
self._clients.append(client)
tool_allowlist = set(server.get("tool_allowlist") or [])
for tool in await client.list_tools():
tool_name = str(tool.get("name") or "")
if not tool_name:
continue
if tool_allowlist and tool_name not in tool_allowlist:
continue
function_name = build_mcp_tool_name(server_name, tool_name)
tool_ref = LiveMCPTool(
function_name=function_name,
server_name=server_name,
tool_name=tool_name,
client=client,
schema=tool,
)
self._tools[function_name] = tool_ref
discovered.append(
{
**tool,
"server_name": server_name,
"function_name": function_name,
}
)
except Exception as exc:
await client.close()
if server.get("required"):
raise
log_and_flush(
logging.WARNING,
f"[MCP] Could not connect server {server_name}: {exc}",
)
return discovered
except Exception as exc:
await client.close()
if server.get("required"):
await self.close()
raise
log_and_flush(
logging.WARNING,
f"[MCP] Could not connect server {server_name}: {exc}",
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/meetingbaas.py` around lines 276 - 320, The connect() flow in
MeetingBaaS leaks already-open MCP clients when a later required server fails,
because the bare re-raise exits before cleanup. Update connect() to close every
client that was successfully added to self._clients before propagating the
error, or ensure setup_mcp_tools/main performs cleanup on connect() failure; use
the connect, self._clients, self._build_client, and log_and_flush symbols to
locate the affected path.

Comment thread utils/mcp_client.py
Comment on lines +88 to +117
def validate_mcp_http_url(url: str) -> None:
"""Block obvious SSRF targets unless explicitly allowed."""
parsed = urlparse(url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise McpClientError(f"Invalid MCP HTTP URL: {url}")

if _private_mcp_urls_allowed():
return

if _is_allowed_private_mcp_url(url):
return

host = parsed.hostname
try:
if _is_private_ip(host):
raise McpClientError(f"MCP HTTP URL host is private or local: {host}")
return
except ValueError:
pass

try:
addresses = socket.getaddrinfo(host, None)
except socket.gaierror as e:
raise McpClientError(f"Could not resolve MCP HTTP host '{host}': {e}") from e

for address in addresses:
if _is_private_ip(address[4][0]):
raise McpClientError(
f"MCP HTTP URL resolves to private or local address: {host}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and relevant symbols.
git ls-files | rg '^utils/mcp_client\.py$'

echo '--- outline ---'
ast-grep outline utils/mcp_client.py --view expanded || true

echo '--- relevant lines ---'
nl -ba utils/mcp_client.py | sed -n '1,260p'

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 3097


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for the prompt_context implementation and any URL validation / private IP handling.
rg -n "prompt_context|validate_mcp_http_url|_is_private_ip|_private_mcp_urls_allowed|_is_allowed_private_mcp_url|allow_redirects|aiohttp" -S utils/mcp_client.py .

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 30163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any helper that might share the same validation/pinning pattern.
rg -n "private.*mcp|DNS rebinding|rebinding|pin.*IP|getaddrinfo|resolve.*private" -S utils . || true

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 1562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path, start, end in [
    ("utils/mcp_client.py", 80, 130),
    ("utils/mcp_client.py", 460, 500),
    ("app/services/prompt_context.py", 140, 180),
    ("tests/test_mcp_client.py", 100, 190),
]:
    print(f"--- {path}:{start}-{end} ---")
    lines = Path(path).read_text().splitlines()
    for i in range(start - 1, min(end, len(lines))):
        print(f"{i+1:4d}: {lines[i]}")
    print()
PY

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 9940


🌐 Web query:

aiohttp DNS resolution connection time re-resolve raw URL connect time SSRF rebinding

💡 Result:

AIOHTTP is susceptible to DNS rebinding attacks if not explicitly configured to prevent them, primarily due to its internal DNS caching mechanism and the Time-of-Check to Time-of-Use (TOCTOU) window between validation and connection [1][2][3]. DNS Rebinding and TOCTOU DNS rebinding exploits the gap between when a URL is validated (e.g., checking if it resolves to a public IP) and when the actual TCP connection is established [3]. If the DNS TTL is short or the cache is bypassed/expired, the hostname may be re-resolved to an attacker-controlled internal or metadata IP (e.g., 127.0.0.1, 169.254.169.254) for the second request, effectively bypassing initial security checks [1][2][3]. Mitigation Strategies 1. Custom Resolver: To prevent TOCTOU, implement a custom resolver that validates IP addresses at the exact moment of connection, rather than relying on pre-request checks [1][2]. By hooking into the TCPConnector, you can ensure that every connection attempt—including those for redirect targets—is validated against an allowlist or blocklist [1][2]. 2. Disable DNS Cache: You can disable the internal DNS cache in AIOHTTP to force resolution on every request, though this alone does not eliminate the TOCTOU window for a single request if it involves redirects or multiple resolution steps [4][5][6]. This is done via TCPConnector(use_dns_cache=False) [7][6]. 3. Redirect Protection: Because AIOHTTP follows redirects by default, SSRF guards must be applied recursively [1][2]. Use custom middleware or hooks to inspect the Location header of redirect responses and validate the destination host before the library follows it [8][9]. 4. IP Literal Validation: Recent updates to AIOHTTP have tightened the handling of IP literals to prevent bypasses where non-canonical numeric forms (e.g., octal or decimal representations) were treated as IP addresses rather than hostnames [10]. Ensure you are using updated versions that correctly canonicalize and validate these inputs [10]. Official Documentation Reference The AIOHTTP TCPConnector provides parameters such as use_dns_cache (boolean) and ttl_dns_cache (integer, in seconds) to manage resolution behavior [11][6]. However, for robust SSRF protection, community best practices emphasize implementing a CustomResolver that performs active validation at connect-time [12][1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import inspect
import sys

try:
    import aiohttp
except Exception as e:
    print(f"IMPORT_ERROR: {type(e).__name__}: {e}")
    sys.exit(0)

print(f"aiohttp_version={getattr(aiohttp, '__version__', 'unknown')}")
mods = []
for qualname in [
    "client.ClientSession._request",
    "connector.TCPConnector._resolve_host",
    "connector.TCPConnector.connect",
]:
    mod_name, attr_path = qualname.split(".", 1)
    obj = getattr(aiohttp, mod_name)
    for part in attr_path.split("."):
        obj = getattr(obj, part)
    print(f"\n--- {qualname} ---")
    try:
        src = inspect.getsource(obj)
        print(src[:6000])
    except Exception as e:
        print(f"SOURCE_ERROR: {type(e).__name__}: {e}")
PY

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 12525


DNS rebinding still bypasses this SSRF check. validate_mcp_http_url() resolves the host once, but _post() passes the original URL to aiohttp, which resolves again when connecting. A rebinding hostname can still reach a private IP; pin the validated address in the connection path or use a resolver/connector that enforces the check at connect time.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 92-92: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 103-103: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 104-104: Consider moving this statement to an else block

(TRY300)


[warning] 111-111: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 115-117: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/mcp_client.py` around lines 88 - 117, DNS rebinding can still slip past
validate_mcp_http_url() because it validates the hostname once, but _post()
later hands the original URL to aiohttp for a second resolution at connect time.
Update the connection path in _post() (and any related aiohttp session/connector
setup) so the validated address is pinned or enforced during connection, using
the existing helpers validate_mcp_http_url(), _is_private_ip(), and
_private_mcp_urls_allowed() as the reference points.

Comment thread utils/mcp_client.py
Comment on lines +384 to +395
async def _request(
self,
method: str,
params: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
await self.start()
assert self._process and self._process.stdin and self._process.stdout
message = self._state.request(method, params)
self._process.stdin.write(encode_stdio_message(message))
await self._process.stdin.drain()
response = await read_stdio_message(self._process.stdout)
return _extract_result(response)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and search for request/response handling.
git ls-files 'utils/mcp_client.py' 'utils/*' | sed -n '1,120p'
echo '--- outline ---'
ast-grep outline utils/mcp_client.py --view expanded || true
echo '--- request-id related search ---'
rg -n "request id|request_id|request_id|id matches|read_stdio_message|encode_stdio_message|_state.request|notification|progress|out of order|stdin|stdout" utils -S || true
echo '--- relevant file excerpt ---'
sed -n '1,260p' utils/mcp_client.py
echo '--- later excerpt ---'
sed -n '260,460p' utils/mcp_client.py

Repository: Meeting-BaaS/speaking-meeting-bot

Length of output: 19745


🌐 Web query:

Model Context Protocol stdio transport notifications progress logs response ordering request id correlation

💡 Result:

The Model Context Protocol (MCP) relies on JSON-RPC 2.0 to manage request-response correlation, message ordering, and transport mechanics [1]. Stdio Transport In the stdio transport, the client launches the MCP server as a subprocess, with communication occurring over standard input (stdin) and standard output (stdout) [2][3]. - Framing: Messages are delimited by newlines and must not contain embedded newlines [2][4]. Each line represents a single valid JSON-RPC request, notification, or response [2]. - Logging: Servers may write UTF-8 strings to stderr for logging, which clients may capture or ignore [2][3]. This is distinct from structured protocol logging [5]. - Exclusivity: The client and server must not write anything to the other's stream that is not a valid MCP message [2][3]. Request ID Correlation MCP uses the JSON-RPC id field to correlate requests and responses [1]. When a client sends a request, it includes an id that the server must mirror in its response to allow the client to match the result to the specific request [1]. Since MCP is stateless, all necessary information is contained within the request itself; servers treat each request independently regardless of connection or process lifetime [6]. Notifications, Progress, and Logs - Request-Scoped Notifications: Certain notifications, specifically notifications/progress and notifications/message (logs), are request-scoped [5][7]. They are delivered on the same response stream as the request they relate to [5][7]. - Progress Tracking: If a client requires progress updates, it includes a progressToken in the request metadata (_meta) [8][9]. The server then sends notifications/progress containing the same token, allowing the client to associate the notification with the active operation [8][9]. - Structured Logging: Clients control logging verbosity per request by including io.modelcontextprotocol/logLevel in the request's _meta [5]. If enabled, the server may send notifications/message on that request's response stream [5]. The server must not emit these for requests lacking the log level field [5]. Response Ordering While MCP messages are delivered sequentially over the stdio stream, the protocol is inherently asynchronous [1]. Because JSON-RPC uses explicit request IDs, clients do not rely on implicit ordering to correlate responses; they can handle interleaved or out-of-order responses by matching them against the corresponding request id [1]. Clients should not assume a specific session or conversation boundary for the stdio process, as unrelated requests may be interleaved [6].

Citations:


Match stdio responses by JSON-RPC id utils/mcp_client.py:384-395 _request consumes the next framed stdout message as the result, so a progress/log notification or another interleaved response can be misattributed. Read until the message id matches the request id, and skip unrelated frames.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 390-390: Assertion should be broken down into multiple parts

Break down assertion into multiple parts

(PT018)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/mcp_client.py` around lines 384 - 395, The _request method in
mcp_client.py currently assumes the next framed stdout message is always the
matching JSON-RPC response, which can misattribute interleaved notifications or
other replies. Update _request to use the request id returned by
self._state.request(method, params), then keep reading from self._process.stdout
with read_stdio_message until a frame with a matching id is found, ignoring
unrelated notifications or responses. Make sure the final _extract_result call
only runs on the response whose id matches the original request.

@Lazare-42

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Lazare-42

Copy link
Copy Markdown
Contributor Author

Superseded by the master feature PR #34 — this branch is 100% contained in it (0 commits outside). Consolidating review there.

@Lazare-42 Lazare-42 closed this Jul 19, 2026
Lazare-42 added a commit that referenced this pull request Jul 21, 2026
Master: external data + MCP + speech speed + LLM routing (consolidates #30/#31/#32)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant