Skip to content

Harden MCP server: fix routing, validation, and usage metering#341

Merged
nicoloboschi merged 6 commits intomainfrom
feat/mcp-mental-models
Feb 11, 2026
Merged

Harden MCP server: fix routing, validation, and usage metering#341
nicoloboschi merged 6 commits intomainfrom
feat/mcp-mental-models

Conversation

@DK09876
Copy link
Contributor

@DK09876 DK09876 commented Feb 10, 2026

Summary

  • Fix 307 redirect bug: Replace Starlette Mount + path-rewrite middleware with a single wrapping MCPMiddleware that intercepts /mcp* requests directly. This prevents POST /mcp (no trailing slash) from getting a 307 redirect that breaks MCP client tool discovery.
  • Fix usage metering for MCP: Move mental model usage metering validators from http.py into memory_engine.py so MCP operations are billed correctly. Fix double validation on create_mental_model and add is_internal checks so background workers skip billing.
  • Harden MCP tools: Add input validation for mental model tools (name, source_query, max_tokens), improve "not found" error messages to include bank_id, remove MCP_ENDPOINTS blocklist that prevented banks named "sse" or "messages", scope SSE body rewriting to text/event-stream only.
  • Add 22 new tests: Integration tests for tool execution, input validation, bank name edge cases; unit tests for _validate_mental_model_inputs helper.

Test plan

  • All 103 tests pass (97 unit + 6 integration)
  • Local server tested with 16 curl URL patterns (no 307, correct routing)
  • 8 real MCP client connection tests (tool discovery, execution, validation)
  • ruff check and ruff format clean

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

DK09876 and others added 5 commits February 10, 2026 16:48
Mental model validation hooks (validate_mental_model_get, validate_mental_model_refresh)
were only called in REST HTTP handlers, not in the engine. MCP tools call engine methods
directly, so usage metering was skipped entirely for MCP mental model operations.

Moved pre-validation and post-completion hooks into memory_engine.py (matching the
retain/recall/reflect pattern) and removed the duplicate code from http.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…al checks

- Remove pre-validation from create_mental_model since callers always call
  submit_async_refresh_mental_model next (which validates), preventing
  double credit checks
- Add is_internal checks to mental model metering validators (matching
  the existing pattern for recall/reflect) so background worker tasks
  skip billing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Starlette's Mount class redirects /mcp to /mcp/ with a 307 Temporary
Redirect. Many MCP clients don't follow POST redirects, which causes
tool discovery to fail (0 tools discovered despite successful auth).

Add _MCPPathRewriteMiddleware that rewrites /mcp to /mcp/ at the ASGI
level before routing, preventing the redirect entirely. Both /mcp and
/mcp/ now work identically.

Add regression test test_mcp_no_trailing_slash_works to verify URLs
with and without trailing slashes discover tools correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove MCP_ENDPOINTS blocklist so banks named "sse"/"messages" route correctly
- Scope SSE body rewriting to text/event-stream responses only to prevent data corruption
- Add _validate_mental_model_inputs for name, source_query, max_tokens validation in MCP tools
- Improve "not found" error messages to include bank_id context
- Fix fragile tool count assertions (exact → minimum bounds)
- Add integration tests: tool execution, input validation, edge-case bank names
- Add unit tests for validation helper and tool-level validation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Starlette's Mount class redirects /mcp -> /mcp/ with 307, which MCP clients
don't follow. Previously we patched this with _MCPPathRewriteMiddleware.

Now MCPMiddleware wraps the FastAPI app directly via add_middleware, intercepting
/mcp* requests before they reach Starlette's router. No Mount means no redirect.

- Remove _MCPPathRewriteMiddleware (no longer needed)
- Remove app.mount() call
- Add prefix parameter to MCPMiddleware
- Use app.add_middleware() for proper Starlette integration
- Simplify path stripping (just remove prefix, no mount/root_path handling)
- Update routing test to match current behavior (no MCP_ENDPOINTS blocklist)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@DK09876 DK09876 requested a review from nicoloboschi February 10, 2026 23:51
…ware

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nicoloboschi nicoloboschi merged commit e798979 into main Feb 11, 2026
26 of 28 checks passed
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.

2 participants