clsplusplus 7.2.3__tar.gz → 7.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/PKG-INFO +2 -1
  2. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/README.md +1 -0
  3. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/pyproject.toml +2 -1
  4. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/api.py +32 -0
  5. clsplusplus-7.2.4/src/clsplusplus/mcp_http.py +235 -0
  6. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/middleware.py +17 -1
  7. clsplusplus-7.2.4/src/clsplusplus/oauth_server.py +444 -0
  8. clsplusplus-7.2.4/src/clsplusplus/stores/oauth_store.py +253 -0
  9. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/PKG-INFO +2 -1
  10. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/SOURCES.txt +4 -0
  11. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/entry_points.txt +1 -0
  12. clsplusplus-7.2.4/tests/test_mcp_oauth.py +692 -0
  13. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/LICENSE +0 -0
  14. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/setup.cfg +0 -0
  15. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/__init__.py +0 -0
  16. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/abuse_guard.py +0 -0
  17. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/api_usage_analytics.py +0 -0
  18. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/auth.py +0 -0
  19. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/cli.py +0 -0
  20. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/client.py +0 -0
  21. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/config.py +0 -0
  22. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/cost_forecast.py +0 -0
  23. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/cost_model.py +0 -0
  24. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/debug_console.py +0 -0
  25. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/demo_llm.py +0 -0
  26. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/demo_llm_calls.py +0 -0
  27. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/demo_local.py +0 -0
  28. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/email_service.py +0 -0
  29. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/embeddings.py +0 -0
  30. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/funnel_routes.py +0 -0
  31. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/geo.py +0 -0
  32. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/health_metrics.py +0 -0
  33. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/homepage_autopromote.py +0 -0
  34. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/idempotency.py +0 -0
  35. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/integration_service.py +0 -0
  36. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/jwt_utils.py +0 -0
  37. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/local_routes.py +0 -0
  38. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/main.py +0 -0
  39. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/mcp_server.py +0 -0
  40. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/memory_cycle.py +0 -0
  41. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/memory_phase.py +0 -0
  42. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/memory_service.py +0 -0
  43. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/__init__.py +0 -0
  44. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/__main__.py +0 -0
  45. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/billing.py +0 -0
  46. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
  47. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/notifier.py +0 -0
  48. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/pricing.py +0 -0
  49. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/reconciler.py +0 -0
  50. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/schema.py +0 -0
  51. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/writer.py +0 -0
  52. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metrics.py +0 -0
  53. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/models.py +0 -0
  54. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/namespace_resolver.py +0 -0
  55. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/permissions.py +0 -0
  56. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/plasticity.py +0 -0
  57. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/pricing.py +0 -0
  58. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/pricing_models.py +0 -0
  59. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/pricing_store.py +0 -0
  60. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/prompt_log.py +0 -0
  61. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/rate_limit.py +0 -0
  62. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/razorpay_service.py +0 -0
  63. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/rbac_service.py +0 -0
  64. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/reconsolidation.py +0 -0
  65. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/resilience.py +0 -0
  66. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/sleep_cycle.py +0 -0
  67. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/__init__.py +0 -0
  68. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/base.py +0 -0
  69. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/chat_session_store.py +0 -0
  70. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/integration_store.py +0 -0
  71. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
  72. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l1_indexing_store.py +0 -0
  73. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
  74. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
  75. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l3_postgres.py +0 -0
  76. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/rbac_store.py +0 -0
  77. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/user_store.py +0 -0
  78. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/waitlist_store.py +0 -0
  79. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/web_events_store.py +0 -0
  80. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stripe_service.py +0 -0
  81. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/subscription_watchdog.py +0 -0
  82. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/temporal.py +0 -0
  83. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/test_suite.py +0 -0
  84. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/tier_resolver.py +0 -0
  85. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/tiers.py +0 -0
  86. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/topical_resonance.py +0 -0
  87. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/tracer.py +0 -0
  88. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/usage.py +0 -0
  89. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/user_embeddings.py +0 -0
  90. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/user_pulse.py +0 -0
  91. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/user_service.py +0 -0
  92. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/waitlist_service.py +0 -0
  93. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/webhook_dispatcher.py +0 -0
  94. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/weblab.py +0 -0
  95. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/weblab_watcher.py +0 -0
  96. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/window_limits.py +0 -0
  97. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
  98. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/requires.txt +0 -0
  99. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/top_level.txt +0 -0
  100. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_abuse_guard.py +0 -0
  101. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_admin.py +0 -0
  102. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_admin_seed.py +0 -0
  103. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api.py +0 -0
  104. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api_comprehensive.py +0 -0
  105. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api_endpoints.py +0 -0
  106. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api_usage_analytics.py +0 -0
  107. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_auth.py +0 -0
  108. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_auth_me_api_key.py +0 -0
  109. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_billing_e2e.py +0 -0
  110. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_burst_hardening.py +0 -0
  111. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_client_sdk.py +0 -0
  112. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_config.py +0 -0
  113. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_cost_forecast.py +0 -0
  114. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_cross_llm_memory.py +0 -0
  115. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_debug_console.py +0 -0
  116. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_demo_llm.py +0 -0
  117. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_embeddings.py +0 -0
  118. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_extension_integration.py +0 -0
  119. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_extension_ui.py +0 -0
  120. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_feedback_github.py +0 -0
  121. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_free_tier_lifecycle.py +0 -0
  122. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_full_api_coverage.py +0 -0
  123. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_funnel_metrics.py +0 -0
  124. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_geo_gating.py +0 -0
  125. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_health_metrics.py +0 -0
  126. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_idempotency.py +0 -0
  127. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_integration_service.py +0 -0
  128. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_mcp_connect.py +0 -0
  129. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_cycle.py +0 -0
  130. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_phase.py +0 -0
  131. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_service.py +0 -0
  132. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_write_no_schema_garbage.py +0 -0
  133. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_healthcheck.py +0 -0
  134. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_notifier.py +0 -0
  135. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_pricing.py +0 -0
  136. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_reconciler.py +0 -0
  137. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_schema.py +0 -0
  138. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_writer.py +0 -0
  139. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_middleware.py +0 -0
  140. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_models.py +0 -0
  141. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_overage_billing.py +0 -0
  142. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_performance.py +0 -0
  143. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_plasticity.py +0 -0
  144. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_pricing.py +0 -0
  145. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_prototype_e2e.py +0 -0
  146. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_rate_limit.py +0 -0
  147. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_razorpay_billing.py +0 -0
  148. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_razorpay_subscription_webhooks.py +0 -0
  149. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_reconsolidation.py +0 -0
  150. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_regression.py +0 -0
  151. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_resilience.py +0 -0
  152. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_security.py +0 -0
  153. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_sleep_cycle.py +0 -0
  154. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_stores.py +0 -0
  155. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_subscription_watchdog.py +0 -0
  156. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_tier_resolver.py +0 -0
  157. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_tiers.py +0 -0
  158. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_usage.py +0 -0
  159. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_auth.py +0 -0
  160. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_embeddings.py +0 -0
  161. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_pulse.py +0 -0
  162. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_stories.py +0 -0
  163. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_waitlist.py +0 -0
  164. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_webhook_dispatcher.py +0 -0
  165. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_weblab.py +0 -0
  166. {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_window_limits.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clsplusplus
3
- Version: 7.2.3
3
+ Version: 7.2.4
4
4
  Summary: Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama.
5
5
  Author-email: AlphaForge AI Labs <contact@alphaforge.ai>
6
6
  Maintainer-email: Rajamohan Jabbala <contact@alphaforge.ai>
@@ -61,6 +61,7 @@ Requires-Dist: httpx>=0.26.0; extra == "dev"
61
61
  Requires-Dist: locust>=2.20.0; extra == "dev"
62
62
  Dynamic: license-file
63
63
 
64
+ <!-- mcp-name: io.github.rajamohan1950/cls-memory -->
64
65
  <p align="center">
65
66
  <img src="https://img.shields.io/badge/CLS%2B%2B-Memory%20for%20LLMs-6366f1?style=for-the-badge&logo=github" alt="CLS++" />
66
67
  </p>
@@ -1,3 +1,4 @@
1
+ <!-- mcp-name: io.github.rajamohan1950/cls-memory -->
1
2
  <p align="center">
2
3
  <img src="https://img.shields.io/badge/CLS%2B%2B-Memory%20for%20LLMs-6366f1?style=for-the-badge&logo=github" alt="CLS++" />
3
4
  </p>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clsplusplus"
7
- version = "7.2.3"
7
+ version = "7.2.4"
8
8
  description = "Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -74,6 +74,7 @@ dev = [
74
74
 
75
75
  [project.scripts]
76
76
  cls = "clsplusplus.cli:main"
77
+ cls-mcp = "clsplusplus.mcp_server:main"
77
78
 
78
79
  [project.urls]
79
80
  Homepage = "https://github.com/rajamohan1950/CLSplusplus"
@@ -146,6 +146,14 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
146
146
  from clsplusplus.namespace_resolver import NamespaceResolver
147
147
  _namespace_resolver = NamespaceResolver(settings)
148
148
 
149
+ # ── Remote MCP server + OAuth 2.1 — claude.ai custom connector ──
150
+ # Shared OAuth store (clients/codes/tokens). The MCP transport at /mcp and
151
+ # the OAuth endpoints are mounted near the end of create_app() so they can
152
+ # reuse the same service instances. Both are gated by a human security
153
+ # review before they go live — see the PR.
154
+ from clsplusplus.stores.oauth_store import OAuthStore
155
+ _oauth_store = OAuthStore(settings)
156
+
149
157
  app = FastAPI(
150
158
  title="CLS++ API",
151
159
  description="Brain-inspired, model-agnostic persistent memory for LLMs",
@@ -2043,6 +2051,26 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
2043
2051
  ),
2044
2052
  }
2045
2053
 
2054
+ # ── Remote MCP server + OAuth 2.1 for claude.ai custom connectors ──
2055
+ # The OAuth authorization server (DCR, authorize, token, metadata) and the
2056
+ # Streamable-HTTP MCP transport at /mcp. Access tokens map to a CLS API key
2057
+ # so /mcp resolves the user's namespace through the same path as direct
2058
+ # API-key auth. These endpoints are added to the middleware allowlist so
2059
+ # AuthMiddleware does not 401 them before they validate their own bearer.
2060
+ from clsplusplus.oauth_server import mount_oauth_routes
2061
+ from clsplusplus.mcp_http import mount_mcp_http
2062
+ mount_oauth_routes(
2063
+ app, settings,
2064
+ oauth_store=_oauth_store,
2065
+ integration_service=integration_service,
2066
+ user_service=user_service,
2067
+ )
2068
+ mount_mcp_http(
2069
+ app, settings,
2070
+ oauth_store=_oauth_store,
2071
+ memory_service=memory_service,
2072
+ )
2073
+
2046
2074
  @app.patch("/v1/user/profile")
2047
2075
  async def update_profile(req: UserProfileUpdateRequest, request: Request):
2048
2076
  """Update user profile (name, email, password)."""
@@ -3811,6 +3839,10 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
3811
3839
  await integration_service.close()
3812
3840
  except Exception:
3813
3841
  pass
3842
+ try:
3843
+ await _oauth_store.close()
3844
+ except Exception:
3845
+ pass
3814
3846
  # Close the shared httpx client used by LLM proxy routes
3815
3847
  try:
3816
3848
  http_client = getattr(_local_router, "_http_client", None)
@@ -0,0 +1,235 @@
1
+ """Remote MCP server — Streamable-HTTP transport for claude.ai.
2
+
3
+ Exposes the same three tools as the stdio server (recall_memories,
4
+ store_memory, who_am_i) over the MCP Streamable-HTTP transport at POST /mcp.
5
+ Requests are authenticated with an OAuth 2.1 Bearer access token (issued by
6
+ oauth_server.py); the token resolves to the user's namespace, and every tool
7
+ call operates only on that namespace.
8
+
9
+ Transport notes:
10
+ * POST /mcp carries one JSON-RPC request/response per call. We return
11
+ application/json (the spec permits a JSON body when no server-initiated
12
+ streaming is needed). Notifications get HTTP 202 with no body.
13
+ * GET /mcp would open an SSE stream for server→client messages. This server
14
+ has no server-initiated traffic, so GET returns 405 (allowed by spec).
15
+ * Unauthenticated requests get 401 with a WWW-Authenticate header pointing at
16
+ the protected-resource metadata, so claude.ai can discover the auth server.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from typing import Optional
23
+
24
+ from fastapi import FastAPI, Request
25
+ from fastapi.responses import JSONResponse, Response
26
+
27
+ from clsplusplus.auth import extract_bearer_token
28
+ from clsplusplus.config import Settings
29
+ from clsplusplus.mcp_server import TOOLS
30
+ from clsplusplus.models import ReadRequest, WriteRequest
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ PROTOCOL_VERSION = "2025-06-18"
35
+ SERVER_INFO = {"name": "cls-memory", "version": "1.0.0"}
36
+
37
+ # who_am_i fans out across these queries to assemble a profile, mirroring the
38
+ # stdio server's behaviour.
39
+ _WHO_AM_I_QUERIES = [
40
+ "user identity name who preferences likes dislikes",
41
+ "relationships family friends people",
42
+ "recent work project decisions current status",
43
+ "movies music hobbies interests favorites perfume",
44
+ ]
45
+
46
+
47
+ def _is_schema_garbage(text: str) -> bool:
48
+ if not text.startswith("[Schema:"):
49
+ return False
50
+ return len(text.split("]", 1)[-1].strip().split()) < 4
51
+
52
+
53
+ def _jsonrpc_result(req_id, result) -> dict:
54
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
55
+
56
+
57
+ def _jsonrpc_error(req_id, code: int, message: str) -> dict:
58
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
59
+
60
+
61
+ def _tool_text_result(text: str) -> dict:
62
+ return {"content": [{"type": "text", "text": text}], "isError": False}
63
+
64
+
65
+ def mount_mcp_http(app: FastAPI, settings: Settings, *, oauth_store, memory_service) -> None:
66
+ """Register the Streamable-HTTP MCP endpoints on the given app."""
67
+
68
+ def _unauthorized(request: Request) -> JSONResponse:
69
+ issuer = (settings.site_base_url or str(request.base_url).rstrip("/")).rstrip("/")
70
+ resource_metadata = f"{issuer}/.well-known/oauth-protected-resource"
71
+ return JSONResponse(
72
+ status_code=401,
73
+ content={"error": "invalid_token",
74
+ "error_description": "A valid OAuth access token is required."},
75
+ headers={
76
+ "WWW-Authenticate": (
77
+ f'Bearer resource_metadata="{resource_metadata}"'
78
+ ),
79
+ },
80
+ )
81
+
82
+ async def _resolve_identity(request: Request) -> Optional[dict]:
83
+ token = extract_bearer_token(request.headers.get("Authorization"))
84
+ if not token:
85
+ return None
86
+ return await oauth_store.resolve_access_token(token)
87
+
88
+ # ── Tool handlers (operate strictly on the authenticated namespace) ──
89
+
90
+ async def _recall_memories(namespace: str, args: dict) -> str:
91
+ query = (args.get("query") or "").strip()
92
+ if not query:
93
+ return "No query provided."
94
+ limit = int(args.get("limit", 5) or 5)
95
+ result = await memory_service.read(
96
+ ReadRequest(query=query, namespace=namespace, limit=limit)
97
+ )
98
+ items = result.items or []
99
+ lines = []
100
+ for item in items:
101
+ text = item.text or ""
102
+ if _is_schema_garbage(text):
103
+ continue
104
+ lines.append(f"- {text}")
105
+ if not lines:
106
+ return "No relevant memories found."
107
+ return "Memories about this user:\n" + "\n".join(lines)
108
+
109
+ async def _store_memory(namespace: str, args: dict) -> str:
110
+ text = (args.get("text") or "").strip()
111
+ if not text:
112
+ return "No text provided to store."
113
+ await memory_service.write(
114
+ WriteRequest(text=text, namespace=namespace, source="mcp")
115
+ )
116
+ return f'Memory stored: "{text}"'
117
+
118
+ async def _who_am_i(namespace: str, args: dict) -> str:
119
+ all_facts: list[str] = []
120
+ seen: set[str] = set()
121
+ for q in _WHO_AM_I_QUERIES:
122
+ result = await memory_service.read(
123
+ ReadRequest(query=q, namespace=namespace, limit=5)
124
+ )
125
+ for item in (result.items or []):
126
+ text = item.text or ""
127
+ if text in seen or _is_schema_garbage(text):
128
+ continue
129
+ seen.add(text)
130
+ all_facts.append(text)
131
+ if not all_facts:
132
+ return ("No memories stored about this user yet. As they share "
133
+ "information, it will be remembered across all AI models.")
134
+ lines = ["Here is everything known about this user from their "
135
+ "conversations across all AI models:"]
136
+ lines.extend(f"- {f}" for f in all_facts)
137
+ return "\n".join(lines)
138
+
139
+ _HANDLERS = {
140
+ "recall_memories": _recall_memories,
141
+ "store_memory": _store_memory,
142
+ "who_am_i": _who_am_i,
143
+ }
144
+
145
+ async def _dispatch(identity: dict, payload: dict):
146
+ """Handle one JSON-RPC message. Returns a dict, or None for notifications."""
147
+ method = payload.get("method", "")
148
+ req_id = payload.get("id")
149
+ params = payload.get("params") or {}
150
+
151
+ if method == "initialize":
152
+ return _jsonrpc_result(req_id, {
153
+ "protocolVersion": PROTOCOL_VERSION,
154
+ "capabilities": {"tools": {"listChanged": False}},
155
+ "serverInfo": SERVER_INFO,
156
+ })
157
+
158
+ if method in ("notifications/initialized", "notifications/cancelled"):
159
+ return None # notification: no response
160
+
161
+ if method == "ping":
162
+ return _jsonrpc_result(req_id, {})
163
+
164
+ if method == "tools/list":
165
+ return _jsonrpc_result(req_id, {"tools": TOOLS})
166
+
167
+ if method == "tools/call":
168
+ tool_name = params.get("name", "")
169
+ arguments = params.get("arguments") or {}
170
+ handler = _HANDLERS.get(tool_name)
171
+ if not handler:
172
+ return _jsonrpc_error(req_id, -32601, f"Unknown tool: {tool_name}")
173
+ try:
174
+ text = await handler(identity["namespace"], arguments)
175
+ except Exception as exc: # surface as an MCP tool error, not transport error
176
+ logger.warning("mcp tool %s failed: %s", tool_name, exc)
177
+ return _jsonrpc_result(req_id, {
178
+ "content": [{"type": "text", "text": f"Error: {exc}"}],
179
+ "isError": True,
180
+ })
181
+ return _jsonrpc_result(req_id, _tool_text_result(text))
182
+
183
+ return _jsonrpc_error(req_id, -32601, f"Method not found: {method}")
184
+
185
+ @app.post("/mcp", include_in_schema=False)
186
+ async def mcp_endpoint(request: Request):
187
+ identity = await _resolve_identity(request)
188
+ if not identity:
189
+ return _unauthorized(request)
190
+
191
+ try:
192
+ payload = await request.json()
193
+ except Exception:
194
+ return JSONResponse(
195
+ status_code=400,
196
+ content=_jsonrpc_error(None, -32700, "Parse error: body is not JSON."),
197
+ )
198
+
199
+ # A JSON-RPC batch is an array; handle each and return the array of
200
+ # non-null responses.
201
+ if isinstance(payload, list):
202
+ responses = []
203
+ for msg in payload:
204
+ r = await _dispatch(identity, msg)
205
+ if r is not None:
206
+ responses.append(r)
207
+ if not responses:
208
+ return Response(status_code=202)
209
+ return JSONResponse(content=responses)
210
+
211
+ if not isinstance(payload, dict):
212
+ return JSONResponse(
213
+ status_code=400,
214
+ content=_jsonrpc_error(None, -32600, "Invalid Request."),
215
+ )
216
+
217
+ result = await _dispatch(identity, payload)
218
+ if result is None:
219
+ return Response(status_code=202)
220
+ return JSONResponse(content=result)
221
+
222
+ @app.get("/mcp", include_in_schema=False)
223
+ async def mcp_get(request: Request):
224
+ # Auth is still required so probes reveal the protected-resource hint.
225
+ identity = await _resolve_identity(request)
226
+ if not identity:
227
+ return _unauthorized(request)
228
+ # No server-initiated streaming — SSE GET stream is not offered.
229
+ return JSONResponse(
230
+ status_code=405,
231
+ content={"error": "method_not_allowed",
232
+ "error_description": "This MCP server has no server-initiated "
233
+ "stream; use POST /mcp."},
234
+ headers={"Allow": "POST"},
235
+ )
@@ -45,6 +45,16 @@ _PUBLIC_PATHS = frozenset({
45
45
  "/v1/waitlist/verify",
46
46
  "/v1/waitlist/stats",
47
47
  "/v1/waitlist/accept",
48
+ # Remote MCP server + OAuth 2.1 (claude.ai custom connector). These do
49
+ # their OWN auth — the OAuth endpoints validate client/PKCE/session, and
50
+ # /mcp validates the OAuth bearer token — so AuthMiddleware must not 401
51
+ # them first. The /mcp and /oauth/* prefixes are matched in _is_public().
52
+ "/mcp",
53
+ "/oauth/register",
54
+ "/oauth/authorize",
55
+ "/oauth/token",
56
+ "/.well-known/oauth-protected-resource",
57
+ "/.well-known/oauth-authorization-server",
48
58
  "/docs",
49
59
  "/redoc",
50
60
  "/openapi.json",
@@ -92,7 +102,13 @@ def _is_public(path: str, method: str) -> bool:
92
102
  if dot_idx != -1 and path[dot_idx:].split("?")[0].lower() in _STATIC_EXTENSIONS:
93
103
  return True
94
104
  return (normalized in _PUBLIC_PATHS
95
- or path.startswith("/docs") or path.startswith("/redoc"))
105
+ or path.startswith("/docs") or path.startswith("/redoc")
106
+ # Remote MCP + OAuth: the prefix forms (e.g. the path-suffixed
107
+ # protected-resource probe) all self-authenticate, so they bypass
108
+ # the API-key/JWT gate here.
109
+ or path.startswith("/mcp")
110
+ or path.startswith("/oauth/")
111
+ or path.startswith("/.well-known/oauth"))
96
112
 
97
113
 
98
114
  def _requires_admin(path: str) -> bool: