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.
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/PKG-INFO +2 -1
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/README.md +1 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/pyproject.toml +2 -1
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/api.py +32 -0
- clsplusplus-7.2.4/src/clsplusplus/mcp_http.py +235 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/middleware.py +17 -1
- clsplusplus-7.2.4/src/clsplusplus/oauth_server.py +444 -0
- clsplusplus-7.2.4/src/clsplusplus/stores/oauth_store.py +253 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/PKG-INFO +2 -1
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/SOURCES.txt +4 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/entry_points.txt +1 -0
- clsplusplus-7.2.4/tests/test_mcp_oauth.py +692 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/LICENSE +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/setup.cfg +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/__init__.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/abuse_guard.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/api_usage_analytics.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/auth.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/cli.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/client.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/config.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/cost_forecast.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/cost_model.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/debug_console.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/demo_llm.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/demo_llm_calls.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/demo_local.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/email_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/embeddings.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/funnel_routes.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/geo.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/health_metrics.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/homepage_autopromote.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/idempotency.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/integration_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/jwt_utils.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/local_routes.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/main.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/mcp_server.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/memory_cycle.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/memory_phase.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/memory_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/__init__.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/__main__.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/billing.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/notifier.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/pricing.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/reconciler.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/schema.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metering_v2/writer.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/metrics.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/models.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/namespace_resolver.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/permissions.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/plasticity.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/pricing.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/pricing_models.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/pricing_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/prompt_log.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/rate_limit.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/razorpay_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/rbac_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/reconsolidation.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/resilience.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/sleep_cycle.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/__init__.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/base.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/chat_session_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/integration_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l1_indexing_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/l3_postgres.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/rbac_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/user_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/waitlist_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stores/web_events_store.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/stripe_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/subscription_watchdog.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/temporal.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/test_suite.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/tier_resolver.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/tiers.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/topical_resonance.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/tracer.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/usage.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/user_embeddings.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/user_pulse.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/user_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/waitlist_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/webhook_dispatcher.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/weblab.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/weblab_watcher.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus/window_limits.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/requires.txt +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/src/clsplusplus.egg-info/top_level.txt +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_abuse_guard.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_admin.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_admin_seed.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api_comprehensive.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api_endpoints.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_api_usage_analytics.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_auth.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_auth_me_api_key.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_billing_e2e.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_burst_hardening.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_client_sdk.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_config.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_cost_forecast.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_cross_llm_memory.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_debug_console.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_demo_llm.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_embeddings.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_extension_integration.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_extension_ui.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_feedback_github.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_free_tier_lifecycle.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_full_api_coverage.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_funnel_metrics.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_geo_gating.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_health_metrics.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_idempotency.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_integration_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_mcp_connect.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_cycle.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_phase.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_service.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_memory_write_no_schema_garbage.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_healthcheck.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_notifier.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_pricing.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_reconciler.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_schema.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_metering_v2_writer.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_middleware.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_models.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_overage_billing.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_performance.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_plasticity.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_pricing.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_prototype_e2e.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_rate_limit.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_razorpay_billing.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_razorpay_subscription_webhooks.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_reconsolidation.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_regression.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_resilience.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_security.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_sleep_cycle.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_stores.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_subscription_watchdog.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_tier_resolver.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_tiers.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_usage.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_auth.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_embeddings.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_pulse.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_user_stories.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_waitlist.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_webhook_dispatcher.py +0 -0
- {clsplusplus-7.2.3 → clsplusplus-7.2.4}/tests/test_weblab.py +0 -0
- {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
|
+
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>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clsplusplus"
|
|
7
|
-
version = "7.2.
|
|
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:
|