Skip to content

fix: harden HTTP MCP risk tools#17

Merged
Vishisht16 merged 2 commits into
Vishisht16:mainfrom
ionfwsrijan:fix/http-mcp-auth-risk-snapshot
May 17, 2026
Merged

fix: harden HTTP MCP risk tools#17
Vishisht16 merged 2 commits into
Vishisht16:mainfrom
ionfwsrijan:fix/http-mcp-auth-risk-snapshot

Conversation

@ionfwsrijan

Copy link
Copy Markdown

Description

Fixes the HTTP MCP security hardening issue by defaulting HTTP MCP to localhost, reusing the existing HUMANE_PROXY_ADMIN_KEY bearer auth pattern, making get_session_risk read-only through a new trajectory snapshot(), and bounding recent escalation queries.

Fixes #15

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the project's style
  • I have added tests for new or changed behaviour
  • All tests pass (pytest tests/ -v)
  • I have updated documentation if needed
  • Self-harm / safety-related changes have been reviewed for sensitivity

Note: focused affected tests pass with 44 passed. Full pytest tests/ -v could not collect locally because fastapi is missing in this environment.

@CLAassistant

CLAassistant commented May 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Security

    • Added optional HTTP Bearer-token protection for the MCP and changed default HTTP bind host to 127.0.0.1; serving on a public host warns if the admin token is not set (configure HUMANE_PROXY_ADMIN_KEY).
  • Documentation

    • README updated with HTTP MCP setup, example commands, and auth instructions.
  • Improvements

    • Session risk endpoints now return stable risk snapshots including message count; escalation queries validate and normalize limits/categories.
  • Tests

    • Added security, validation, and integration tests covering MCP, risk snapshots, and escalation query behavior.

Walkthrough

Hardens HTTP MCP: defaults HTTP bind to localhost and adds optional Bearer-token auth, introduces read-only trajectory snapshot()/to_dict(), validates/clamps escalation query inputs, updates integrations and admin API to use snapshots, adjusts CLI/README, and adds tests for auth, validation, and snapshot read-only behavior.

Changes

Security hardening and read-only risk snapshot

Layer / File(s) Summary
Risk snapshot and trajectory helpers
humane_proxy/risk/trajectory.py
_trend_for_scores() and _category_counts() extract previously inline logic, analyze() is refactored to use them, and two new public APIs are introduced: snapshot(session_id) returns current trajectory state without mutation, and to_dict(result) serializes TrajectoryResult to a dict.
Escalation query validation
humane_proxy/escalation/query.py
New module defines ALLOWED_ESCALATION_CATEGORIES, DEFAULT_ESCALATION_LIMIT, MAX_ESCALATION_LIMIT, and normalize_escalation_query(limit, category) which validates category membership and clamps limit to safe bounds.
MCP authentication and default host changes
humane_proxy/mcp_server.py
New constants MCP_TOKEN_ENV/MCP_DEFAULT_HOST; helper to build optional FastMCP BearerTokenAuth from HUMANE_PROXY_ADMIN_KEY; FastMCP receives auth when present; serve_http() defaults to 127.0.0.1 and warns when binding publicly without a token.
MCP tool implementations using snapshot and validated queries
humane_proxy/mcp_server.py
get_session_risk() now returns to_dict(snapshot(session_id)); list_recent_escalations() normalizes (limit, category) via normalize_escalation_query() before querying the store.
Admin API adjustments
humane_proxy/api/admin.py
GET /admin/sessions/{id}/risk uses snapshot() and always includes trajectory with message_count; DELETE /admin/sessions/{id} now declares response_class=Response and returns Response(status_code=204).
AutoGen, CrewAI, LlamaIndex integration updates
humane_proxy/integrations/autogen.py, humane_proxy/integrations/crewai.py, humane_proxy/integrations/llamaindex.py
Integrations updated to use snapshot()/to_dict() for risk queries and normalize_escalation_query() for escalation listing inputs.
CLI defaults and security documentation
humane_proxy/cli.py, README.md
mcp-serve --host default changed to 127.0.0.1 with updated help; README adds guidance that exposing HTTP MCP publicly requires HUMANE_PROXY_ADMIN_KEY, example commands, and updated tool descriptions.
Security and snapshot test coverage
tests/test_mcp_security.py, tests/test_integrations_smoke.py, tests/test_trajectory.py, tests/test_admin_api.py
New tests verify MCP defaults and auth provider behavior, normalize_escalation_query validation/clamping and error cases, _is_public_bind_host, snapshot read-only behavior, and integration tool read-only guarantees.
sequenceDiagram
    participant HTTPClient
    participant FastMCP
    participant BearerTokenAuth
    participant GetSessionRiskTool
    participant snapshot
    participant to_dict
    participant HTTPResponse

    HTTPClient->>FastMCP: POST /call/get_session_risk
    alt HUMANE_PROXY_ADMIN_KEY is set
        FastMCP->>BearerTokenAuth: validate Authorization Bearer token
        alt token valid
            BearerTokenAuth->>FastMCP: authenticated
        else token missing or invalid
            BearerTokenAuth->>HTTPResponse: 403 Forbidden
        end
    else no HUMANE_PROXY_ADMIN_KEY
        FastMCP->>GetSessionRiskTool: proceed (no auth)
    end
    GetSessionRiskTool->>snapshot: snapshot(session_id)
    Note over snapshot: read-only, no side effects
    snapshot->>to_dict: TrajectoryResult -> dict
    to_dict->>GetSessionRiskTool: dict with spike_detected, trend, scores, counts
    GetSessionRiskTool->>HTTPResponse: JSON response
    HTTPResponse->>HTTPClient: 200 OK + risk snapshot
Loading

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐰 A snapshot's taken, no scores rearranged,
Tokens now guard what's stored and exchanged,
Local by default, public with care,
Escalations bounded, trajectories fair!
Read-only and safe, the risks now laid bare.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: harden HTTP MCP risk tools' directly summarizes the main change: security hardening of HTTP MCP by addressing risk tool vulnerabilities.
Description check ✅ Passed The description clearly relates to the changeset by explaining the HTTP MCP security hardening fixes including localhost binding, bearer auth, read-only snapshots, and escalation query bounds.
Linked Issues check ✅ Passed The PR implements all key acceptance criteria from issue #15: HTTP MCP defaults to 127.0.0.1, bearer token auth is configured, snapshot() provides read-only risk access, agent integrations use snapshots, escalation queries are validated and bounded, and tests demonstrate the hardening.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the HTTP MCP security hardening objectives: localhost binding, auth setup, read-only snapshots, escalation validation, integration updates, tests, and documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/test_mcp_security.py (1)

19-19: ⚡ Quick win

Fragile test: accessing __defaults__[0] couples to parameter order.

If the serve_http signature changes (e.g., a new parameter is added before host), the index will shift and this assertion will break or check the wrong default.

Consider testing the actual behavior by calling serve_http() without arguments in a controlled environment, or use inspect.signature() to verify the default by parameter name.

♻️ More robust alternative using inspect
+import inspect
+
 def test_http_mcp_defaults_to_localhost():
     assert MCP_DEFAULT_HOST == "127.0.0.1"
-    assert serve_http.__defaults__[0] == "127.0.0.1"
+    sig = inspect.signature(serve_http)
+    assert sig.parameters['host'].default == "127.0.0.1"
🤖 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 `@tests/test_mcp_security.py` at line 19, The test directly indexes
serve_http.__defaults__[0], which is fragile because parameter order can change;
update the test to assert the default for the host parameter by name using
inspect.signature(serve_http).parameters['host'].default or, alternatively,
exercise real behavior by calling serve_http() without arguments in a controlled
environment and asserting it binds to "127.0.0.1"; locate the assertion
referencing serve_http.__defaults__ and replace it with the inspect-based lookup
or the no-arg invocation to make the test robust to signature reordering.
🤖 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 `@humane_proxy/escalation/query.py`:
- Around line 21-27: The code currently sets normalized_category =
category.strip() if category else None but leaves whitespace-only strings as ""
instead of None; change this so that after stripping you treat empty strings as
None (e.g., set normalized_category = category.strip() if category and
category.strip() else None), then keep the existing membership check against
ALLOWED_ESCALATION_CATEGORIES using normalized_category and return
normalized_limit and normalized_category; this ensures whitespace-only inputs
become None and don't narrow queries.

In `@humane_proxy/mcp_server.py`:
- Around line 190-196: The current check only flags hosts in _PUBLIC_BIND_HOSTS
(e.g. "0.0.0.0"/"::") so concrete non-loopback IPs (like 192.168.x.x) bypass the
warning; change the condition to detect any non-loopback, non-local bind
instead: use the host variable and ipaddress.ip_address(host) (with try/except
for names) to determine if the address is a loopback/unspecified; if the host is
not loopback/unspecified and os.environ.get(MCP_TOKEN_ENV, "").strip() is empty,
emit the same logger.warning (retain the existing message and MCP_TOKEN_ENV
reference) so all non-loopback binds trigger the token warning instead of only
_PUBLIC_BIND_HOSTS.

In `@humane_proxy/risk/trajectory.py`:
- Around line 215-227: snapshot() currently hardcodes spike_detected=False and
thus discards any previously detected spike state; change it to read the spike
flag from the stored history tuples instead: use the second element of the most
recent entry in session_history (the same deque assigned to history) to set
spike_detected (falling back to False if history is empty). Update the
TrajectoryResult construction in snapshot() to use that derived spike_detected
value while keeping trend/_trend_for_scores(scores), window_scores,
category_counts(_category_counts(session_id)) and message_count logic unchanged.

---

Nitpick comments:
In `@tests/test_mcp_security.py`:
- Line 19: The test directly indexes serve_http.__defaults__[0], which is
fragile because parameter order can change; update the test to assert the
default for the host parameter by name using
inspect.signature(serve_http).parameters['host'].default or, alternatively,
exercise real behavior by calling serve_http() without arguments in a controlled
environment and asserting it binds to "127.0.0.1"; locate the assertion
referencing serve_http.__defaults__ and replace it with the inspect-based lookup
or the no-arg invocation to make the test robust to signature reordering.
🪄 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: CHILL

Plan: Pro

Run ID: 2a07be66-abd0-45fd-a810-546f5cdd0034

📥 Commits

Reviewing files that changed from the base of the PR and between 9168b30 and 5adf1af.

📒 Files selected for processing (11)
  • README.md
  • humane_proxy/cli.py
  • humane_proxy/escalation/query.py
  • humane_proxy/integrations/autogen.py
  • humane_proxy/integrations/crewai.py
  • humane_proxy/integrations/llamaindex.py
  • humane_proxy/mcp_server.py
  • humane_proxy/risk/trajectory.py
  • tests/test_integrations_smoke.py
  • tests/test_mcp_security.py
  • tests/test_trajectory.py

Comment thread humane_proxy/escalation/query.py Outdated
Comment thread humane_proxy/mcp_server.py Outdated
Comment thread humane_proxy/risk/trajectory.py
@Vishisht16

Copy link
Copy Markdown
Owner

@ionfwsrijan good work refactoring trajectory.py. But there are still some issues I want you to address in another commit.

  1. snapshot() has a hardcoded spike_detected=False. You can use a _last_spike_by_session that is initialised in analyze() and read by snapshot().
  2. You added read-only tests for LlamaIndex and AutoGen but not for CrewAI, so please add them too.
  3. Whitespace category isn't being converted to None in query.py.
  4. CodeRabbit's suggestion to broaden public bind detection can also be implemented in mcp_server.py using ipaddress.
  5. Please also update get_session_risk in admin.py to use snapshot() as well, instead of analyze() and _admin_replay workaround.

Note: You must sign the CLA provided in the conversation for your code to be merged. Also, please check issues raised by CodeRabbit as inline comments after raising a PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
humane_proxy/api/admin.py (1)

373-390: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Purge in-memory trajectory state on session delete.

DELETE /admin/sessions/{session_id} removes DB rows but does not clear trajectory state. Because get_session_risk now uses snapshot(session_id), deleted sessions can still expose prior risk metadata from memory. Please clear per-session trajectory/snapshot state in this handler (or via a dedicated erasure helper) alongside the SQL delete.

🤖 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 `@humane_proxy/api/admin.py` around lines 373 - 390, The delete_session_data
handler removes DB rows but doesn't clear the in-memory trajectory/snapshot, so
callers of get_session_risk (which uses snapshot(session_id)) can still see
erased data; update delete_session_data to also purge the in-memory state after
the SQL delete by invoking the snapshot/trajectory erasure API used elsewhere
(e.g. call the existing snapshot-clearing helper or manager — something like
clear_session_snapshot(session_id) or trajectory_store.delete(session_id) —
immediately after the DB DELETE and before returning), or add a small erasure
helper and call it from delete_session_data to ensure per-session
trajectory/snapshot state is removed from memory.
🧹 Nitpick comments (1)
tests/test_integrations_smoke.py (1)

88-92: ⚡ Quick win

Assert the AutoGen risk payload too, not only no-mutation behavior.

get_session_risk(sid) is called but its returned data isn’t verified, so wrong payload regressions can slip through.

Proposed fix
-    get_session_risk(sid)
+    result = get_session_risk(sid)
 
+    assert result["message_count"] == before_count
     assert len(session_history[sid]) == before_count
     assert len(_category_history[sid]) == before_count
🤖 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 `@tests/test_integrations_smoke.py` around lines 88 - 92, The test currently
calls get_session_risk(sid) but doesn't verify its return value; update the test
to capture the result (e.g., risk = get_session_risk(sid)) and add assertions
that the returned payload has the expected shape and values (check required
keys/fields and their types/values relevant to this scenario) in addition to the
existing immutability checks on session_history[sid] and _category_history[sid];
reference get_session_risk, sid, before_count, session_history and
_category_history when adding these assertions.
🤖 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 `@tests/test_admin_api.py`:
- Around line 126-130: The test uses a fixed sid ("sess-1") which can leak
global trajectory state and cause flaky tests; change the test to generate a
unique session id for each run (e.g., using uuid.uuid4() or pytest's
tmp_path/fixture) and replace the hardcoded sid variable used in the
analyze(...) calls so each invocation of analyze(sid, ...) uses a fresh unique
id; ensure the variable named sid and all analyze(...) calls in this test are
updated accordingly.

---

Outside diff comments:
In `@humane_proxy/api/admin.py`:
- Around line 373-390: The delete_session_data handler removes DB rows but
doesn't clear the in-memory trajectory/snapshot, so callers of get_session_risk
(which uses snapshot(session_id)) can still see erased data; update
delete_session_data to also purge the in-memory state after the SQL delete by
invoking the snapshot/trajectory erasure API used elsewhere (e.g. call the
existing snapshot-clearing helper or manager — something like
clear_session_snapshot(session_id) or trajectory_store.delete(session_id) —
immediately after the DB DELETE and before returning), or add a small erasure
helper and call it from delete_session_data to ensure per-session
trajectory/snapshot state is removed from memory.

---

Nitpick comments:
In `@tests/test_integrations_smoke.py`:
- Around line 88-92: The test currently calls get_session_risk(sid) but doesn't
verify its return value; update the test to capture the result (e.g., risk =
get_session_risk(sid)) and add assertions that the returned payload has the
expected shape and values (check required keys/fields and their types/values
relevant to this scenario) in addition to the existing immutability checks on
session_history[sid] and _category_history[sid]; reference get_session_risk,
sid, before_count, session_history and _category_history when adding these
assertions.
🪄 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: CHILL

Plan: Pro

Run ID: f6cf233b-4d8f-4b36-ac42-1f189e66d112

📥 Commits

Reviewing files that changed from the base of the PR and between 5adf1af and bafdd96.

📒 Files selected for processing (8)
  • humane_proxy/api/admin.py
  • humane_proxy/escalation/query.py
  • humane_proxy/mcp_server.py
  • humane_proxy/risk/trajectory.py
  • tests/test_admin_api.py
  • tests/test_integrations_smoke.py
  • tests/test_mcp_security.py
  • tests/test_trajectory.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • humane_proxy/escalation/query.py
  • tests/test_trajectory.py

Comment thread tests/test_admin_api.py
@ionfwsrijan

Copy link
Copy Markdown
Author

@Vishisht16 I've made the changes. You may merge it now.

@Vishisht16 Vishisht16 added bug Something isn't working backend Issues involving server-side logic, databases, or APIs gssoc:approved Approved PR under GSSoC'26 level:critical Irrespective of difficulty, core work for the project type:docs Documentation change type:security Fixes security issues type:design Design changes in code type:refactor Multiple files refactored type:bug Smashes annoying bugs quality:exceptional Bonus points under GSSoC for Exceptional PR labels May 17, 2026
@Vishisht16 Vishisht16 merged commit c2e0825 into Vishisht16:main May 17, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Issues involving server-side logic, databases, or APIs bug Something isn't working gssoc:approved Approved PR under GSSoC'26 level:critical Irrespective of difficulty, core work for the project quality:exceptional Bonus points under GSSoC for Exceptional PR type:bug Smashes annoying bugs type:design Design changes in code type:docs Documentation change type:refactor Multiple files refactored type:security Fixes security issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security hardening: protect HTTP MCP tools and make risk snapshot reads side-effect free

3 participants