@macaw-ai/client 0.0.0-stage → 0.9.11
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 +422 -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,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SecureMCP @secure Decorator - Simplified Version
|
|
3
|
+
Delegates all security to MACAW SDK components from server instance
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import threading
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Dict, Any, Optional, List, Callable, Union
|
|
9
|
+
|
|
10
|
+
# Thread-local storage for current server instance
|
|
11
|
+
_current_server = threading.local()
|
|
12
|
+
|
|
13
|
+
def set_current_server(server):
|
|
14
|
+
"""Set the current server in thread-local storage"""
|
|
15
|
+
_current_server.value = server
|
|
16
|
+
|
|
17
|
+
def get_current_server():
|
|
18
|
+
"""Get the current server from thread-local storage"""
|
|
19
|
+
return getattr(_current_server, 'value', None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def secure(
|
|
23
|
+
# Core security parameters
|
|
24
|
+
policy: Union[str, Dict[str, Any]] = None,
|
|
25
|
+
policy_file: Optional[str] = None,
|
|
26
|
+
audit_log: bool = False,
|
|
27
|
+
audit_level: str = "basic",
|
|
28
|
+
|
|
29
|
+
# Authentication
|
|
30
|
+
auth_provider: Optional[str] = None,
|
|
31
|
+
required_roles: Optional[List[str]] = None,
|
|
32
|
+
required_groups: Optional[List[str]] = None,
|
|
33
|
+
|
|
34
|
+
# Rate limiting
|
|
35
|
+
rate_limit: Optional[int] = None,
|
|
36
|
+
|
|
37
|
+
# Signing
|
|
38
|
+
sign_requests: bool = False,
|
|
39
|
+
sign_results: bool = False,
|
|
40
|
+
|
|
41
|
+
# Monitoring
|
|
42
|
+
monitor: bool = False,
|
|
43
|
+
trace: bool = False,
|
|
44
|
+
|
|
45
|
+
# Advanced features (for future use)
|
|
46
|
+
attested: bool = False,
|
|
47
|
+
tenant_aware: bool = False,
|
|
48
|
+
risk_assessment: Optional[Dict[str, Any]] = None,
|
|
49
|
+
llm_security: Optional[Dict[str, Any]] = None,
|
|
50
|
+
**kwargs # Accept other params for compatibility
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Optional security enhancement decorator for MCP tools.
|
|
54
|
+
|
|
55
|
+
This decorator adds fine-grained security controls on top of the baseline
|
|
56
|
+
security provided by SecureMCP's transport layer. All security enforcement
|
|
57
|
+
is delegated to MACAW SDK components.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
policy: Security policy name or dict
|
|
61
|
+
policy_file: Path to policy file
|
|
62
|
+
audit_log: Enable detailed audit logging
|
|
63
|
+
audit_level: Level of audit detail (basic/detailed/full)
|
|
64
|
+
auth_provider: Authentication provider (okta/github/auth0/etc)
|
|
65
|
+
required_roles: List of required roles
|
|
66
|
+
required_groups: List of required groups
|
|
67
|
+
rate_limit: Calls per minute limit
|
|
68
|
+
sign_requests: Sign incoming requests
|
|
69
|
+
sign_results: Sign outgoing results
|
|
70
|
+
monitor: Enable monitoring
|
|
71
|
+
trace: Enable tracing
|
|
72
|
+
attested: Require attested workflow
|
|
73
|
+
tenant_aware: Enable multi-tenancy
|
|
74
|
+
risk_assessment: Risk assessment config
|
|
75
|
+
llm_security: LLM security config
|
|
76
|
+
**kwargs: Additional parameters for future expansion
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
@server.tool("sensitive_operation")
|
|
80
|
+
@secure(
|
|
81
|
+
auth_provider="okta",
|
|
82
|
+
required_roles=["admin"],
|
|
83
|
+
audit_log=True,
|
|
84
|
+
rate_limit=10
|
|
85
|
+
)
|
|
86
|
+
def sensitive_operation(data: str) -> str:
|
|
87
|
+
return process_data(data)
|
|
88
|
+
"""
|
|
89
|
+
def decorator(func: Callable) -> Callable:
|
|
90
|
+
@wraps(func)
|
|
91
|
+
def wrapper(*args, **kwargs):
|
|
92
|
+
# Get server instance
|
|
93
|
+
server = get_current_server()
|
|
94
|
+
if not server:
|
|
95
|
+
# No server context, just run the function
|
|
96
|
+
return func(*args, **kwargs)
|
|
97
|
+
|
|
98
|
+
# Get MACAW components from server
|
|
99
|
+
policy_enforcer = getattr(server, 'policy_enforcer', None)
|
|
100
|
+
audit_logger = getattr(server, 'audit_logger', None)
|
|
101
|
+
server_agent = getattr(server, 'server_agent', None)
|
|
102
|
+
|
|
103
|
+
# Build security context
|
|
104
|
+
security_context = {
|
|
105
|
+
'function': func.__name__,
|
|
106
|
+
'policy': policy,
|
|
107
|
+
'auth_provider': auth_provider,
|
|
108
|
+
'required_roles': required_roles,
|
|
109
|
+
'required_groups': required_groups,
|
|
110
|
+
'rate_limit': rate_limit,
|
|
111
|
+
'attested': attested
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# 1. Authentication checks (delegated to MACAW)
|
|
115
|
+
if auth_provider or required_roles or required_groups:
|
|
116
|
+
# In production, MACAW's AuthenticationService handles this
|
|
117
|
+
# For now, we'll check if server has auth configured
|
|
118
|
+
if not getattr(server, 'request_authenticator', None):
|
|
119
|
+
# Log warning but don't block
|
|
120
|
+
if audit_logger:
|
|
121
|
+
audit_logger.log_event({
|
|
122
|
+
'type': 'auth_check_skipped',
|
|
123
|
+
'reason': 'no_authenticator',
|
|
124
|
+
'function': func.__name__
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
# 2. Policy enforcement (delegated to MACAW)
|
|
128
|
+
if policy and policy_enforcer:
|
|
129
|
+
# MACAW's PolicyEnforcer handles this
|
|
130
|
+
# Create an invocation context
|
|
131
|
+
from macaw.protocol.mcp_types import Invocation
|
|
132
|
+
invocation = Invocation(
|
|
133
|
+
tool_name=func.__name__,
|
|
134
|
+
params=kwargs,
|
|
135
|
+
metadata=security_context
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Check policy
|
|
139
|
+
try:
|
|
140
|
+
allowed = policy_enforcer.check_invocation(invocation)
|
|
141
|
+
if not allowed:
|
|
142
|
+
return {
|
|
143
|
+
'error': 'Policy violation',
|
|
144
|
+
'policy': policy,
|
|
145
|
+
'function': func.__name__
|
|
146
|
+
}
|
|
147
|
+
except Exception as e:
|
|
148
|
+
# Policy check failed, log but don't block
|
|
149
|
+
if audit_logger:
|
|
150
|
+
audit_logger.log_event({
|
|
151
|
+
'type': 'policy_check_error',
|
|
152
|
+
'error': str(e),
|
|
153
|
+
'function': func.__name__
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
# 3. Rate limiting (delegated to MACAW)
|
|
157
|
+
if rate_limit:
|
|
158
|
+
# MACAW's RateLimitVerifier handles this
|
|
159
|
+
# For now, just log the intent
|
|
160
|
+
if audit_logger:
|
|
161
|
+
audit_logger.log_event({
|
|
162
|
+
'type': 'rate_limit_check',
|
|
163
|
+
'limit': rate_limit,
|
|
164
|
+
'function': func.__name__
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
# 4. Pre-execution audit
|
|
168
|
+
if audit_log and audit_logger:
|
|
169
|
+
audit_logger.log_event({
|
|
170
|
+
'type': 'function_invocation',
|
|
171
|
+
'function': func.__name__,
|
|
172
|
+
'audit_level': audit_level,
|
|
173
|
+
'security_context': security_context,
|
|
174
|
+
'args_count': len(args),
|
|
175
|
+
'kwargs_keys': list(kwargs.keys())
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
# 5. Sign request if needed
|
|
179
|
+
if sign_requests and server_agent:
|
|
180
|
+
# MACAW's agent handles signing
|
|
181
|
+
# This would be done at transport layer
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# 6. Execute the function
|
|
185
|
+
try:
|
|
186
|
+
result = func(*args, **kwargs)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
# Log error and re-raise
|
|
189
|
+
if audit_logger:
|
|
190
|
+
audit_logger.log_event({
|
|
191
|
+
'type': 'function_error',
|
|
192
|
+
'function': func.__name__,
|
|
193
|
+
'error': str(e)
|
|
194
|
+
})
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
# 7. Sign result if needed
|
|
198
|
+
if sign_results and server_agent:
|
|
199
|
+
# MACAW's agent handles signing
|
|
200
|
+
# This would be done at transport layer
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
# 8. Post-execution audit
|
|
204
|
+
if audit_log and audit_logger:
|
|
205
|
+
audit_logger.log_event({
|
|
206
|
+
'type': 'function_completed',
|
|
207
|
+
'function': func.__name__,
|
|
208
|
+
'success': True
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
# 9. Monitoring/tracing
|
|
212
|
+
if (monitor or trace) and hasattr(server, 'telemetry_manager'):
|
|
213
|
+
# MACAW's telemetry handles this
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
# Mark function as secure-decorated for introspection
|
|
219
|
+
wrapper._secure_decorated = True
|
|
220
|
+
wrapper._secure_params = {
|
|
221
|
+
'policy': policy,
|
|
222
|
+
'auth_provider': auth_provider,
|
|
223
|
+
'required_roles': required_roles,
|
|
224
|
+
'audit_log': audit_log
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return wrapper
|
|
228
|
+
return decorator
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastMCP Compatibility Layer for SecureMCP
|
|
3
|
+
|
|
4
|
+
Provides FastMCP-style features and API compatibility to make migration easier.
|
|
5
|
+
This allows developers familiar with FastMCP to use SecureMCP with minimal changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional, Dict, Any, Callable, Type, get_type_hints, Union
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TypeValidator:
|
|
19
|
+
"""
|
|
20
|
+
Automatic type validation for tool parameters.
|
|
21
|
+
|
|
22
|
+
FastMCP feature: Validates parameters against type hints automatically.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def validate_params(func: Callable, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Validate and coerce parameters based on function type hints.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
func: Function with type hints
|
|
32
|
+
params: Parameters to validate
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Validated and coerced parameters
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TypeError: If validation fails
|
|
39
|
+
"""
|
|
40
|
+
# Get type hints
|
|
41
|
+
hints = get_type_hints(func)
|
|
42
|
+
sig = inspect.signature(func)
|
|
43
|
+
|
|
44
|
+
validated = {}
|
|
45
|
+
|
|
46
|
+
for param_name, param_info in sig.parameters.items():
|
|
47
|
+
if param_name == "self":
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# Get value from params or use default
|
|
51
|
+
if param_name in params:
|
|
52
|
+
value = params[param_name]
|
|
53
|
+
elif param_info.default != inspect.Parameter.empty:
|
|
54
|
+
value = param_info.default
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError(f"Missing required parameter: {param_name}")
|
|
57
|
+
|
|
58
|
+
# Get expected type
|
|
59
|
+
if param_name in hints:
|
|
60
|
+
expected_type = hints[param_name]
|
|
61
|
+
|
|
62
|
+
# Handle Optional types
|
|
63
|
+
if hasattr(expected_type, '__origin__') and expected_type.__origin__ is Union:
|
|
64
|
+
# This is Optional[T] or Union[T, None]
|
|
65
|
+
if value is not None:
|
|
66
|
+
# Get the non-None type
|
|
67
|
+
types = expected_type.__args__
|
|
68
|
+
expected_type = next(t for t in types if t != type(None))
|
|
69
|
+
else:
|
|
70
|
+
validated[param_name] = None
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Try to coerce type
|
|
74
|
+
if not isinstance(value, expected_type):
|
|
75
|
+
try:
|
|
76
|
+
if expected_type == int:
|
|
77
|
+
value = int(value)
|
|
78
|
+
elif expected_type == float:
|
|
79
|
+
value = float(value)
|
|
80
|
+
elif expected_type == str:
|
|
81
|
+
value = str(value)
|
|
82
|
+
elif expected_type == bool:
|
|
83
|
+
value = bool(value)
|
|
84
|
+
else:
|
|
85
|
+
raise TypeError(f"Cannot coerce {value} to {expected_type}")
|
|
86
|
+
except (ValueError, TypeError) as e:
|
|
87
|
+
raise TypeError(f"Parameter {param_name}: expected {expected_type.__name__}, got {type(value).__name__}")
|
|
88
|
+
|
|
89
|
+
validated[param_name] = value
|
|
90
|
+
|
|
91
|
+
return validated
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FastMCPServer:
|
|
95
|
+
"""
|
|
96
|
+
FastMCP-compatible server wrapper.
|
|
97
|
+
|
|
98
|
+
Adds FastMCP-style features on top of SecureMCP Server.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, server):
|
|
102
|
+
"""
|
|
103
|
+
Wrap a SecureMCP server with FastMCP compatibility.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
server: SecureMCP Server instance
|
|
107
|
+
"""
|
|
108
|
+
self.server = server
|
|
109
|
+
self._original_tool = server.tool
|
|
110
|
+
|
|
111
|
+
# Override tool decorator to add type validation
|
|
112
|
+
server.tool = self.tool
|
|
113
|
+
|
|
114
|
+
logger.info("FastMCP compatibility layer initialized")
|
|
115
|
+
|
|
116
|
+
def tool(self, name: Optional[str] = None, description: str = ""):
|
|
117
|
+
"""
|
|
118
|
+
Enhanced tool decorator with automatic type validation.
|
|
119
|
+
|
|
120
|
+
FastMCP feature: Automatically validates types based on hints.
|
|
121
|
+
"""
|
|
122
|
+
def decorator(func: Callable) -> Callable:
|
|
123
|
+
# Get the original function for type hints
|
|
124
|
+
original_func = func
|
|
125
|
+
|
|
126
|
+
# Create wrapper that validates types
|
|
127
|
+
def validated_wrapper(**kwargs):
|
|
128
|
+
# Validate and coerce types
|
|
129
|
+
validated_params = TypeValidator.validate_params(original_func, kwargs)
|
|
130
|
+
|
|
131
|
+
# Call original function with validated params
|
|
132
|
+
return original_func(**validated_params)
|
|
133
|
+
|
|
134
|
+
# Copy metadata
|
|
135
|
+
validated_wrapper.__name__ = original_func.__name__
|
|
136
|
+
validated_wrapper.__doc__ = original_func.__doc__
|
|
137
|
+
|
|
138
|
+
# Register with server using original decorator
|
|
139
|
+
return self._original_tool(name, description)(validated_wrapper)
|
|
140
|
+
|
|
141
|
+
return decorator
|
|
142
|
+
|
|
143
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
|
144
|
+
"""
|
|
145
|
+
Call a tool programmatically (dependency injection).
|
|
146
|
+
|
|
147
|
+
FastMCP feature: Allows tools to call other tools.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
tool_name: Name of tool to call
|
|
151
|
+
arguments: Tool arguments
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Tool result
|
|
155
|
+
"""
|
|
156
|
+
if tool_name not in self.server.tools:
|
|
157
|
+
raise ValueError(f"Unknown tool: {tool_name}")
|
|
158
|
+
|
|
159
|
+
tool_info = self.server.tools[tool_name]
|
|
160
|
+
function = tool_info["function"]
|
|
161
|
+
|
|
162
|
+
# Call the tool
|
|
163
|
+
result = function(**arguments)
|
|
164
|
+
|
|
165
|
+
# Handle async functions
|
|
166
|
+
if asyncio.iscoroutine(result):
|
|
167
|
+
result = await result
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
@asynccontextmanager
|
|
172
|
+
async def test_client(self):
|
|
173
|
+
"""
|
|
174
|
+
Create a test client for the server.
|
|
175
|
+
|
|
176
|
+
FastMCP feature: Easy testing with context manager.
|
|
177
|
+
|
|
178
|
+
Usage:
|
|
179
|
+
async with server.test_client() as client:
|
|
180
|
+
result = await client.call_tool("my-tool", {"arg": "value"})
|
|
181
|
+
"""
|
|
182
|
+
from . import Client
|
|
183
|
+
|
|
184
|
+
# Create a test client
|
|
185
|
+
client = Client(f"test-client-{self.server.name}")
|
|
186
|
+
|
|
187
|
+
# Create a mock connection (in-process)
|
|
188
|
+
class TestConnection:
|
|
189
|
+
def __init__(self, server):
|
|
190
|
+
self.server = server
|
|
191
|
+
|
|
192
|
+
async def call_tool(self, name: str, args: Dict[str, Any]) -> Any:
|
|
193
|
+
"""Direct in-process tool call"""
|
|
194
|
+
return await self.server.call_tool(name, args)
|
|
195
|
+
|
|
196
|
+
async def get_resource(self, uri: str) -> Any:
|
|
197
|
+
"""Direct in-process resource read"""
|
|
198
|
+
if uri in self.server.server.resources:
|
|
199
|
+
resource_info = self.server.server.resources[uri]
|
|
200
|
+
resource_class = resource_info["class"]
|
|
201
|
+
instance = resource_class(uri)
|
|
202
|
+
if hasattr(instance, 'read'):
|
|
203
|
+
return await instance.read()
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
async def get_prompt(self, name: str, args: Dict[str, Any]) -> str:
|
|
207
|
+
"""Direct in-process prompt generation"""
|
|
208
|
+
if name in self.server.server.prompts:
|
|
209
|
+
prompt_info = self.server.server.prompts[name]
|
|
210
|
+
function = prompt_info["function"]
|
|
211
|
+
return function(**args)
|
|
212
|
+
return ""
|
|
213
|
+
|
|
214
|
+
# Attach test connection to client
|
|
215
|
+
test_conn = TestConnection(self)
|
|
216
|
+
client.call_tool = test_conn.call_tool
|
|
217
|
+
client.get_resource = test_conn.get_resource
|
|
218
|
+
client.get_prompt = test_conn.get_prompt
|
|
219
|
+
client.connected = True
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
yield client
|
|
223
|
+
finally:
|
|
224
|
+
# Cleanup
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass
|
|
229
|
+
class ErrorTemplate:
|
|
230
|
+
"""
|
|
231
|
+
Standardized error response template.
|
|
232
|
+
|
|
233
|
+
FastMCP feature: Consistent error formatting.
|
|
234
|
+
"""
|
|
235
|
+
code: str
|
|
236
|
+
message: str
|
|
237
|
+
details: Optional[Dict[str, Any]] = None
|
|
238
|
+
|
|
239
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
240
|
+
"""Convert to MCP error format"""
|
|
241
|
+
result = {
|
|
242
|
+
"error": {
|
|
243
|
+
"code": self.code,
|
|
244
|
+
"message": self.message
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if self.details:
|
|
248
|
+
result["error"]["details"] = self.details
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Pagination:
|
|
253
|
+
"""
|
|
254
|
+
Pagination helper for resources.
|
|
255
|
+
|
|
256
|
+
FastMCP feature: Built-in pagination support.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def paginate(
|
|
261
|
+
items: list,
|
|
262
|
+
page: int = 1,
|
|
263
|
+
page_size: int = 10
|
|
264
|
+
) -> Dict[str, Any]:
|
|
265
|
+
"""
|
|
266
|
+
Paginate a list of items.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
items: List to paginate
|
|
270
|
+
page: Page number (1-based)
|
|
271
|
+
page_size: Items per page
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Paginated response with metadata
|
|
275
|
+
"""
|
|
276
|
+
total = len(items)
|
|
277
|
+
total_pages = (total + page_size - 1) // page_size
|
|
278
|
+
|
|
279
|
+
# Calculate slice
|
|
280
|
+
start = (page - 1) * page_size
|
|
281
|
+
end = start + page_size
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"items": items[start:end],
|
|
285
|
+
"pagination": {
|
|
286
|
+
"page": page,
|
|
287
|
+
"page_size": page_size,
|
|
288
|
+
"total": total,
|
|
289
|
+
"total_pages": total_pages,
|
|
290
|
+
"has_next": page < total_pages,
|
|
291
|
+
"has_prev": page > 1
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def add_fastmcp_compat(server):
|
|
297
|
+
"""
|
|
298
|
+
Add FastMCP compatibility to a SecureMCP server.
|
|
299
|
+
|
|
300
|
+
Usage:
|
|
301
|
+
from macaw_adapters.mcp import Server
|
|
302
|
+
from macaw_adapters.mcp.fastmcp_compat import add_fastmcp_compat
|
|
303
|
+
|
|
304
|
+
server = Server("my-server")
|
|
305
|
+
server = add_fastmcp_compat(server)
|
|
306
|
+
|
|
307
|
+
# Now use FastMCP features
|
|
308
|
+
@server.tool("typed-tool")
|
|
309
|
+
def my_tool(path: str, count: int = 10):
|
|
310
|
+
# Types are automatically validated!
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
server: SecureMCP Server instance
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Server with FastMCP compatibility
|
|
318
|
+
"""
|
|
319
|
+
compat = FastMCPServer(server)
|
|
320
|
+
|
|
321
|
+
# Add methods to server
|
|
322
|
+
server.call_tool = compat.call_tool
|
|
323
|
+
server.test_client = compat.test_client
|
|
324
|
+
|
|
325
|
+
# Add helper classes
|
|
326
|
+
server.ErrorTemplate = ErrorTemplate
|
|
327
|
+
server.Pagination = Pagination
|
|
328
|
+
|
|
329
|
+
return server
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# Convenience decorators for standalone use
|
|
333
|
+
def typed_tool(name: Optional[str] = None, description: str = ""):
|
|
334
|
+
"""
|
|
335
|
+
Standalone typed tool decorator.
|
|
336
|
+
|
|
337
|
+
Usage:
|
|
338
|
+
@typed_tool("calculate")
|
|
339
|
+
def calculate(a: float, b: float) -> float:
|
|
340
|
+
return a + b
|
|
341
|
+
"""
|
|
342
|
+
def decorator(func: Callable) -> Callable:
|
|
343
|
+
# Create wrapper that validates types
|
|
344
|
+
def validated_wrapper(**kwargs):
|
|
345
|
+
validated_params = TypeValidator.validate_params(func, kwargs)
|
|
346
|
+
return func(**validated_params)
|
|
347
|
+
|
|
348
|
+
validated_wrapper.__name__ = func.__name__
|
|
349
|
+
validated_wrapper.__doc__ = func.__doc__
|
|
350
|
+
|
|
351
|
+
return validated_wrapper
|
|
352
|
+
|
|
353
|
+
return decorator
|