@macaw-ai/client 0.0.0-stage → 0.9.10
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.
- package/LICENSE.md +339 -0
- package/bin/install.js +278 -0
- package/package.json +37 -4
- package/payload/README.md +56 -0
- package/payload/secureAI/LICENSE +190 -0
- package/payload/secureAI/NOTICE +5 -0
- package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
- package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
- package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
- package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
- package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
- package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
- package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
- package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
- package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
- package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
- package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
- package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
- package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
- package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
- package/payload/secureAI/examples/README.md +244 -0
- package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
- package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
- package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
- package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
- package/payload/secureAI/examples/attestations/README.md +128 -0
- package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
- package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
- package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
- package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
- package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
- package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
- package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
- package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
- package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
- package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
- package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
- package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
- package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
- package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
- package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
- package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
- package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
- package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
- package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
- package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
- package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
- package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
- package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
- package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
- package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
- package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
- package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
- package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
- package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
- package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
- package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
- package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
- package/payload/secureAI/macaw_adapters/__init__.py +55 -0
- package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
- package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
- package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
- package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
- package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
- package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
- package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
- package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
- package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
- package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
- package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
- package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
- package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
- package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
- package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
- package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
- package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
- package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
- package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
- package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
- package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
- package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
- package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
- package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
- package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
- package/payload/secureAI/test_harness_quick.py +296 -0
- package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource Subscriptions for SecureMCP
|
|
3
|
+
|
|
4
|
+
Enables clients to subscribe to resource changes and receive real-time updates.
|
|
5
|
+
When MACAW is available, updates are signed for authenticity.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Dict, Any, List, Callable, Optional, Set
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SubscriptionEvent(Enum):
|
|
20
|
+
"""Types of subscription events"""
|
|
21
|
+
CREATED = "created"
|
|
22
|
+
UPDATED = "updated"
|
|
23
|
+
DELETED = "deleted"
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Subscription:
|
|
29
|
+
"""Represents a resource subscription"""
|
|
30
|
+
subscription_id: str
|
|
31
|
+
client_id: str
|
|
32
|
+
resource_uri: str
|
|
33
|
+
created_at: datetime
|
|
34
|
+
last_update: Optional[datetime] = None
|
|
35
|
+
event_types: Set[SubscriptionEvent] = field(default_factory=lambda: {SubscriptionEvent.UPDATED})
|
|
36
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
def matches_event(self, event_type: SubscriptionEvent) -> bool:
|
|
39
|
+
"""Check if subscription should receive this event type"""
|
|
40
|
+
return event_type in self.event_types
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ResourceUpdate:
|
|
45
|
+
"""Represents a resource update event"""
|
|
46
|
+
resource_uri: str
|
|
47
|
+
event_type: SubscriptionEvent
|
|
48
|
+
data: Any
|
|
49
|
+
timestamp: datetime
|
|
50
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
signature: Optional[str] = None # Signature when MACAW available
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
"""Convert to dictionary for transmission"""
|
|
55
|
+
return {
|
|
56
|
+
"resource_uri": self.resource_uri,
|
|
57
|
+
"event_type": self.event_type.value,
|
|
58
|
+
"data": self.data,
|
|
59
|
+
"timestamp": self.timestamp.isoformat(),
|
|
60
|
+
"metadata": self.metadata,
|
|
61
|
+
"signature": self.signature
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SubscriptionManager:
|
|
66
|
+
"""
|
|
67
|
+
Manages resource subscriptions for a SecureMCP server.
|
|
68
|
+
|
|
69
|
+
This class handles:
|
|
70
|
+
- Subscription registration/unregistration
|
|
71
|
+
- Event distribution to subscribers
|
|
72
|
+
- Subscription lifecycle management
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, server=None):
|
|
76
|
+
"""
|
|
77
|
+
Initialize subscription manager.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
server: SecureMCP Server instance
|
|
81
|
+
"""
|
|
82
|
+
self.server = server
|
|
83
|
+
self.subscriptions: Dict[str, Subscription] = {}
|
|
84
|
+
self.resource_subscriptions: Dict[str, List[str]] = {} # resource_uri -> subscription_ids
|
|
85
|
+
self.client_subscriptions: Dict[str, List[str]] = {} # client_id -> subscription_ids
|
|
86
|
+
self.update_handlers: Dict[str, Callable] = {} # client_id -> handler function
|
|
87
|
+
|
|
88
|
+
logger.info("SubscriptionManager initialized")
|
|
89
|
+
|
|
90
|
+
async def subscribe(
|
|
91
|
+
self,
|
|
92
|
+
client_id: str,
|
|
93
|
+
resource_uri: str,
|
|
94
|
+
event_types: Optional[List[SubscriptionEvent]] = None,
|
|
95
|
+
handler: Optional[Callable] = None
|
|
96
|
+
) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Subscribe a client to resource updates.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
client_id: ID of subscribing client
|
|
102
|
+
resource_uri: URI pattern of resource to subscribe to
|
|
103
|
+
event_types: Types of events to subscribe to
|
|
104
|
+
handler: Callback function for updates
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Subscription ID
|
|
108
|
+
"""
|
|
109
|
+
subscription_id = f"sub-{uuid.uuid4().hex[:8]}"
|
|
110
|
+
|
|
111
|
+
# Create subscription
|
|
112
|
+
subscription = Subscription(
|
|
113
|
+
subscription_id=subscription_id,
|
|
114
|
+
client_id=client_id,
|
|
115
|
+
resource_uri=resource_uri,
|
|
116
|
+
created_at=datetime.now(),
|
|
117
|
+
event_types=set(event_types) if event_types else {SubscriptionEvent.UPDATED}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Store subscription
|
|
121
|
+
self.subscriptions[subscription_id] = subscription
|
|
122
|
+
|
|
123
|
+
# Index by resource
|
|
124
|
+
if resource_uri not in self.resource_subscriptions:
|
|
125
|
+
self.resource_subscriptions[resource_uri] = []
|
|
126
|
+
self.resource_subscriptions[resource_uri].append(subscription_id)
|
|
127
|
+
|
|
128
|
+
# Index by client
|
|
129
|
+
if client_id not in self.client_subscriptions:
|
|
130
|
+
self.client_subscriptions[client_id] = []
|
|
131
|
+
self.client_subscriptions[client_id].append(subscription_id)
|
|
132
|
+
|
|
133
|
+
# Store handler if provided
|
|
134
|
+
if handler:
|
|
135
|
+
self.update_handlers[client_id] = handler
|
|
136
|
+
|
|
137
|
+
logger.info(f"Client {client_id} subscribed to {resource_uri}: {subscription_id}")
|
|
138
|
+
|
|
139
|
+
return subscription_id
|
|
140
|
+
|
|
141
|
+
async def unsubscribe(self, subscription_id: str) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Unsubscribe from resource updates.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
subscription_id: ID of subscription to remove
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True if unsubscribed successfully
|
|
150
|
+
"""
|
|
151
|
+
if subscription_id not in self.subscriptions:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
subscription = self.subscriptions[subscription_id]
|
|
155
|
+
|
|
156
|
+
# Remove from indices
|
|
157
|
+
if subscription.resource_uri in self.resource_subscriptions:
|
|
158
|
+
self.resource_subscriptions[subscription.resource_uri].remove(subscription_id)
|
|
159
|
+
if not self.resource_subscriptions[subscription.resource_uri]:
|
|
160
|
+
del self.resource_subscriptions[subscription.resource_uri]
|
|
161
|
+
|
|
162
|
+
if subscription.client_id in self.client_subscriptions:
|
|
163
|
+
self.client_subscriptions[subscription.client_id].remove(subscription_id)
|
|
164
|
+
if not self.client_subscriptions[subscription.client_id]:
|
|
165
|
+
del self.client_subscriptions[subscription.client_id]
|
|
166
|
+
|
|
167
|
+
# Remove subscription
|
|
168
|
+
del self.subscriptions[subscription_id]
|
|
169
|
+
|
|
170
|
+
logger.info(f"Unsubscribed: {subscription_id}")
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
async def unsubscribe_client(self, client_id: str) -> int:
|
|
174
|
+
"""
|
|
175
|
+
Unsubscribe all subscriptions for a client.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
client_id: Client to unsubscribe
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Number of subscriptions removed
|
|
182
|
+
"""
|
|
183
|
+
if client_id not in self.client_subscriptions:
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
subscription_ids = self.client_subscriptions[client_id].copy()
|
|
187
|
+
count = 0
|
|
188
|
+
|
|
189
|
+
for subscription_id in subscription_ids:
|
|
190
|
+
if await self.unsubscribe(subscription_id):
|
|
191
|
+
count += 1
|
|
192
|
+
|
|
193
|
+
# Remove handler
|
|
194
|
+
if client_id in self.update_handlers:
|
|
195
|
+
del self.update_handlers[client_id]
|
|
196
|
+
|
|
197
|
+
return count
|
|
198
|
+
|
|
199
|
+
async def notify_update(
|
|
200
|
+
self,
|
|
201
|
+
resource_uri: str,
|
|
202
|
+
event_type: SubscriptionEvent,
|
|
203
|
+
data: Any,
|
|
204
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
205
|
+
):
|
|
206
|
+
"""
|
|
207
|
+
Notify subscribers of a resource update.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
resource_uri: URI of updated resource
|
|
211
|
+
event_type: Type of event
|
|
212
|
+
data: Update data
|
|
213
|
+
metadata: Additional metadata
|
|
214
|
+
"""
|
|
215
|
+
# Create update event
|
|
216
|
+
update = ResourceUpdate(
|
|
217
|
+
resource_uri=resource_uri,
|
|
218
|
+
event_type=event_type,
|
|
219
|
+
data=data,
|
|
220
|
+
timestamp=datetime.now(),
|
|
221
|
+
metadata=metadata or {}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Sign update if MACAW available
|
|
225
|
+
if self.server and hasattr(self.server, 'dual_signing'):
|
|
226
|
+
signed = self.server.dual_signing.sign_message(update.to_dict())
|
|
227
|
+
if signed:
|
|
228
|
+
update.signature = signed.signature
|
|
229
|
+
|
|
230
|
+
# Find matching subscriptions
|
|
231
|
+
matching_subs = self._find_matching_subscriptions(resource_uri, event_type)
|
|
232
|
+
|
|
233
|
+
# Notify each subscriber
|
|
234
|
+
for subscription_id in matching_subs:
|
|
235
|
+
subscription = self.subscriptions[subscription_id]
|
|
236
|
+
await self._send_update(subscription, update)
|
|
237
|
+
|
|
238
|
+
def _find_matching_subscriptions(
|
|
239
|
+
self,
|
|
240
|
+
resource_uri: str,
|
|
241
|
+
event_type: SubscriptionEvent
|
|
242
|
+
) -> List[str]:
|
|
243
|
+
"""
|
|
244
|
+
Find subscriptions that match a resource update.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
resource_uri: URI of updated resource
|
|
248
|
+
event_type: Type of event
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of matching subscription IDs
|
|
252
|
+
"""
|
|
253
|
+
matching = []
|
|
254
|
+
|
|
255
|
+
# Check exact matches
|
|
256
|
+
if resource_uri in self.resource_subscriptions:
|
|
257
|
+
for sub_id in self.resource_subscriptions[resource_uri]:
|
|
258
|
+
subscription = self.subscriptions[sub_id]
|
|
259
|
+
if subscription.matches_event(event_type):
|
|
260
|
+
matching.append(sub_id)
|
|
261
|
+
|
|
262
|
+
# Check pattern matches (simplified - real implementation would do glob matching)
|
|
263
|
+
for pattern, sub_ids in self.resource_subscriptions.items():
|
|
264
|
+
if "*" in pattern:
|
|
265
|
+
# Simple wildcard matching
|
|
266
|
+
pattern_base = pattern.replace("*", "")
|
|
267
|
+
if resource_uri.startswith(pattern_base):
|
|
268
|
+
for sub_id in sub_ids:
|
|
269
|
+
subscription = self.subscriptions[sub_id]
|
|
270
|
+
if subscription.matches_event(event_type) and sub_id not in matching:
|
|
271
|
+
matching.append(sub_id)
|
|
272
|
+
|
|
273
|
+
return matching
|
|
274
|
+
|
|
275
|
+
async def _send_update(self, subscription: Subscription, update: ResourceUpdate):
|
|
276
|
+
"""
|
|
277
|
+
Send an update to a subscriber.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
subscription: Subscription to notify
|
|
281
|
+
update: Update to send
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
# Update subscription timestamp
|
|
285
|
+
subscription.last_update = datetime.now()
|
|
286
|
+
|
|
287
|
+
# Get handler for client
|
|
288
|
+
handler = self.update_handlers.get(subscription.client_id)
|
|
289
|
+
|
|
290
|
+
if handler:
|
|
291
|
+
# Call handler with update
|
|
292
|
+
await handler(update.to_dict())
|
|
293
|
+
logger.debug(f"Sent update to {subscription.client_id} for {update.resource_uri}")
|
|
294
|
+
else:
|
|
295
|
+
logger.warning(f"No handler for client {subscription.client_id}")
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"Failed to send update to {subscription.client_id}: {e}")
|
|
299
|
+
|
|
300
|
+
def get_subscriptions(self, client_id: Optional[str] = None) -> List[Subscription]:
|
|
301
|
+
"""
|
|
302
|
+
Get subscriptions, optionally filtered by client.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
client_id: Optional client ID to filter by
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
List of subscriptions
|
|
309
|
+
"""
|
|
310
|
+
if client_id:
|
|
311
|
+
sub_ids = self.client_subscriptions.get(client_id, [])
|
|
312
|
+
return [self.subscriptions[sid] for sid in sub_ids]
|
|
313
|
+
else:
|
|
314
|
+
return list(self.subscriptions.values())
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class SubscriptionClient:
|
|
318
|
+
"""
|
|
319
|
+
Client-side subscription handler for SecureMCP.
|
|
320
|
+
|
|
321
|
+
Manages subscriptions and handles incoming updates.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(self, client):
|
|
325
|
+
"""
|
|
326
|
+
Initialize subscription client.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
client: SecureMCP Client instance
|
|
330
|
+
"""
|
|
331
|
+
self.client = client
|
|
332
|
+
self.subscriptions: Dict[str, Subscription] = {}
|
|
333
|
+
self.update_callbacks: Dict[str, Callable] = {} # resource_uri -> callback
|
|
334
|
+
|
|
335
|
+
logger.info(f"SubscriptionClient initialized for {client.client_id}")
|
|
336
|
+
|
|
337
|
+
async def subscribe(
|
|
338
|
+
self,
|
|
339
|
+
resource_uri: str,
|
|
340
|
+
callback: Callable,
|
|
341
|
+
event_types: Optional[List[SubscriptionEvent]] = None
|
|
342
|
+
) -> str:
|
|
343
|
+
"""
|
|
344
|
+
Subscribe to resource updates.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
resource_uri: Resource to subscribe to
|
|
348
|
+
callback: Function to call on updates
|
|
349
|
+
event_types: Types of events to subscribe to
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Subscription ID
|
|
353
|
+
"""
|
|
354
|
+
# Store callback
|
|
355
|
+
self.update_callbacks[resource_uri] = callback
|
|
356
|
+
|
|
357
|
+
# Send subscription request to server
|
|
358
|
+
if hasattr(self.client, 'stdio_client'):
|
|
359
|
+
# STDIO transport
|
|
360
|
+
result = await self.client.stdio_client.send_request(
|
|
361
|
+
"resources/subscribe",
|
|
362
|
+
{
|
|
363
|
+
"uri": resource_uri,
|
|
364
|
+
"client_id": self.client.client_id,
|
|
365
|
+
"event_types": [e.value for e in (event_types or [SubscriptionEvent.UPDATED])]
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
subscription_id = result.get("subscription_id")
|
|
369
|
+
else:
|
|
370
|
+
# Would implement for other transports
|
|
371
|
+
subscription_id = f"sub-{uuid.uuid4().hex[:8]}"
|
|
372
|
+
|
|
373
|
+
# Store subscription
|
|
374
|
+
subscription = Subscription(
|
|
375
|
+
subscription_id=subscription_id,
|
|
376
|
+
client_id=self.client.client_id,
|
|
377
|
+
resource_uri=resource_uri,
|
|
378
|
+
created_at=datetime.now(),
|
|
379
|
+
event_types=set(event_types) if event_types else {SubscriptionEvent.UPDATED}
|
|
380
|
+
)
|
|
381
|
+
self.subscriptions[subscription_id] = subscription
|
|
382
|
+
|
|
383
|
+
logger.info(f"Subscribed to {resource_uri}: {subscription_id}")
|
|
384
|
+
return subscription_id
|
|
385
|
+
|
|
386
|
+
async def handle_update(self, update_data: Dict[str, Any]):
|
|
387
|
+
"""
|
|
388
|
+
Handle an incoming resource update.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
update_data: Update data from server
|
|
392
|
+
"""
|
|
393
|
+
resource_uri = update_data.get("resource_uri")
|
|
394
|
+
|
|
395
|
+
# Verify signature if present
|
|
396
|
+
if "signature" in update_data and hasattr(self.client, 'dual_signing'):
|
|
397
|
+
# Would verify server signature here
|
|
398
|
+
logger.debug(f"Received signed update for {resource_uri}")
|
|
399
|
+
|
|
400
|
+
# Find callback
|
|
401
|
+
callback = self.update_callbacks.get(resource_uri)
|
|
402
|
+
|
|
403
|
+
# Also check pattern matches
|
|
404
|
+
if not callback:
|
|
405
|
+
for pattern, cb in self.update_callbacks.items():
|
|
406
|
+
if "*" in pattern:
|
|
407
|
+
pattern_base = pattern.replace("*", "")
|
|
408
|
+
if resource_uri.startswith(pattern_base):
|
|
409
|
+
callback = cb
|
|
410
|
+
break
|
|
411
|
+
|
|
412
|
+
if callback:
|
|
413
|
+
try:
|
|
414
|
+
await callback(update_data)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error(f"Callback error for {resource_uri}: {e}")
|
|
417
|
+
else:
|
|
418
|
+
logger.warning(f"No callback for resource update: {resource_uri}")
|
|
419
|
+
|
|
420
|
+
async def unsubscribe(self, subscription_id: str) -> bool:
|
|
421
|
+
"""
|
|
422
|
+
Unsubscribe from resource updates.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
subscription_id: Subscription to cancel
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
True if unsubscribed successfully
|
|
429
|
+
"""
|
|
430
|
+
if subscription_id not in self.subscriptions:
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
subscription = self.subscriptions[subscription_id]
|
|
434
|
+
|
|
435
|
+
# Send unsubscribe request to server
|
|
436
|
+
if hasattr(self.client, 'stdio_client'):
|
|
437
|
+
await self.client.stdio_client.send_request(
|
|
438
|
+
"resources/unsubscribe",
|
|
439
|
+
{"subscription_id": subscription_id}
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Remove callback
|
|
443
|
+
if subscription.resource_uri in self.update_callbacks:
|
|
444
|
+
del self.update_callbacks[subscription.resource_uri]
|
|
445
|
+
|
|
446
|
+
# Remove subscription
|
|
447
|
+
del self.subscriptions[subscription_id]
|
|
448
|
+
|
|
449
|
+
logger.info(f"Unsubscribed: {subscription_id}")
|
|
450
|
+
return True
|