@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.
Files changed (142) hide show
  1. package/LICENSE.md +339 -0
  2. package/bin/install.js +278 -0
  3. package/package.json +37 -4
  4. package/payload/README.md +56 -0
  5. package/payload/secureAI/LICENSE +190 -0
  6. package/payload/secureAI/NOTICE +5 -0
  7. package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
  8. package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
  9. package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
  10. package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
  11. package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
  12. package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
  13. package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
  14. package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
  15. package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
  16. package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
  17. package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
  18. package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
  19. package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
  20. package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
  21. package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
  22. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
  23. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
  24. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
  25. package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
  26. package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
  27. package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
  28. package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
  29. package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
  30. package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
  31. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
  32. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
  33. package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
  34. package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
  35. package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
  36. package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
  37. package/payload/secureAI/examples/README.md +244 -0
  38. package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
  39. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
  40. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
  41. package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
  42. package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
  43. package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
  44. package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
  45. package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
  46. package/payload/secureAI/examples/attestations/README.md +128 -0
  47. package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
  48. package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
  49. package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
  50. package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
  51. package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
  52. package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
  53. package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
  54. package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
  55. package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
  56. package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
  57. package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
  58. package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
  59. package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
  60. package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
  61. package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
  62. package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
  63. package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
  64. package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
  65. package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
  66. package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
  67. package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
  68. package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
  69. package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
  70. package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
  71. package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
  72. package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
  73. package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
  74. package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
  75. package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
  76. package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
  77. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
  78. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
  79. package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
  80. package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
  81. package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
  82. package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
  83. package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
  84. package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
  85. package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
  86. package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
  87. package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
  88. package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
  89. package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
  90. package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
  91. package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
  92. package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
  93. package/payload/secureAI/macaw_adapters/__init__.py +55 -0
  94. package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
  95. package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
  96. package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
  97. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
  98. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
  99. package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
  100. package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
  101. package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
  102. package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
  103. package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
  104. package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
  105. package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
  106. package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
  107. package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
  108. package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
  109. package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
  110. package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
  111. package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
  112. package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
  113. package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
  114. package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
  115. package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
  116. package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
  117. package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
  118. package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
  119. package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
  120. package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
  121. package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
  122. package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
  123. package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
  124. package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
  125. package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
  126. package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
  127. package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
  128. package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
  129. package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
  130. package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
  131. package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
  132. package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
  133. package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
  134. package/payload/secureAI/test_harness_quick.py +296 -0
  135. package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
  136. package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
  137. package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
  138. package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
  139. package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
  140. package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
  141. package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
  142. package/README.md +0 -3
@@ -0,0 +1,626 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SecureMCPProxy - Inline Gateway for External MCP Servers.
4
+
5
+ Wraps any third-party MCP server (Salesforce, Google, Slack, etc.)
6
+ with MACAW security: policy enforcement, cryptographic signing, audit.
7
+
8
+ This is an "inline gateway" - it runs in your process and proxies calls
9
+ to external MCP servers through MACAW's security layer.
10
+
11
+ Supports two transport modes:
12
+ - HTTP: Connect to remote MCP servers via HTTP/SSE
13
+ - stdio: Connect to local MCP servers via subprocess stdin/stdout
14
+
15
+ Usage (HTTP):
16
+ from macaw_adapters.mcp import SecureMCPProxy
17
+
18
+ proxy = SecureMCPProxy(
19
+ app_name="salesforce-mcp",
20
+ upstream_url="https://mcp.salesforce.com",
21
+ upstream_auth={"type": "bearer", "token": SF_TOKEN}
22
+ )
23
+
24
+ Usage (stdio):
25
+ proxy = SecureMCPProxy(
26
+ app_name="salesforce-dx",
27
+ command=["npx", "@salesforce/mcp", "--orgs", "DEFAULT"],
28
+ env={"HOME": os.environ["HOME"]}
29
+ )
30
+
31
+ Common API (transport-agnostic):
32
+ tools = proxy.list_tools()
33
+ result = proxy.call_tool("query_accounts", {"limit": 10})
34
+ user_proxy = proxy.bind_to_user(user_client)
35
+
36
+ Security Model:
37
+ - MACAW creates AuthenticatedContext for EACH invocation
38
+ - Even with persistent stdio, each call has fresh identity/policy/signature
39
+ - This solves the "shared context" problem in standard MCP
40
+
41
+ Install: pip install macaw-adapters[mcp-proxy]
42
+ """
43
+
44
+ import asyncio
45
+ import logging
46
+ from typing import Dict, Any, Optional, List
47
+ from dataclasses import dataclass, field
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ def _tool_schema(tool) -> dict:
53
+ """Input schema of an upstream tool.
54
+
55
+ mcp>=2 renamed Tool.inputSchema to Tool.input_schema. Reading the wrong one
56
+ yields no error, just an empty schema - and an empty schema means the proxy
57
+ registers the tool with no parameter constraints for policy to bind to.
58
+ """
59
+ return (
60
+ getattr(tool, "input_schema", None)
61
+ or getattr(tool, "inputSchema", None)
62
+ or {}
63
+ )
64
+
65
+
66
+ @dataclass
67
+ class UpstreamAuth:
68
+ """Authentication configuration for upstream MCP server."""
69
+ type: str = "none" # "none", "bearer", "api_key", "oauth"
70
+ token: Optional[str] = None
71
+ api_key: Optional[str] = None
72
+ header_name: str = "Authorization" # For custom header names
73
+
74
+
75
+ class SecureMCPProxy:
76
+ """
77
+ Inline gateway that wraps external MCP servers with MACAW security.
78
+
79
+ Connects to any MCP server (Salesforce, Google, Slack, etc.) and:
80
+ - Auto-discovers available tools via MCP protocol
81
+ - Re-exposes tools through MACAWClient (policy, signing, audit)
82
+ - Supports multi-user identity via bind_to_user()
83
+
84
+ Transports:
85
+ - HTTP: For remote MCP servers (Snowflake, Databricks, Google Cloud)
86
+ - stdio: For local MCP servers run as subprocesses (Salesforce DX CLI)
87
+
88
+ Security Model:
89
+ - MACAW creates AuthenticatedContext for EACH invocation
90
+ - Even with persistent stdio subprocess, each call gets fresh context
91
+ - Policy enforcement, signing, and audit happen per-invocation
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ app_name: str,
97
+ # HTTP transport options
98
+ upstream_url: Optional[str] = None,
99
+ upstream_auth: Optional[Dict[str, Any]] = None,
100
+ # stdio transport options
101
+ command: Optional[List[str]] = None,
102
+ env: Optional[Dict[str, str]] = None,
103
+ # Identity (for single-user or service mode)
104
+ iam_token: Optional[str] = None,
105
+ user_name: Optional[str] = None,
106
+ # Optional intent policy
107
+ intent_policy: Optional[Dict[str, Any]] = None,
108
+ ):
109
+ """
110
+ Initialize SecureMCPProxy.
111
+
112
+ Provide either upstream_url (HTTP) or command (stdio), not both.
113
+
114
+ Args:
115
+ app_name: Application name for MACAW registration (e.g., "salesforce-mcp")
116
+
117
+ HTTP transport:
118
+ upstream_url: URL of the upstream MCP server
119
+ upstream_auth: Authentication config:
120
+ {"type": "bearer", "token": "..."} or
121
+ {"type": "api_key", "api_key": "...", "header_name": "X-API-Key"}
122
+
123
+ stdio transport:
124
+ command: Command to start MCP server (e.g., ["npx", "@salesforce/mcp"])
125
+ env: Environment variables for subprocess
126
+
127
+ Common:
128
+ iam_token: Optional IAM token for user identity
129
+ user_name: Optional user name
130
+ intent_policy: Optional MACAW intent policy
131
+ """
132
+ self.app_name = app_name
133
+ self.intent_policy = intent_policy or {}
134
+
135
+ # Determine transport
136
+ if command and upstream_url:
137
+ raise ValueError("Provide either upstream_url (HTTP) or command (stdio), not both")
138
+ elif command:
139
+ self._transport = "stdio"
140
+ self._stdio_command = command
141
+ self._stdio_env = env or {}
142
+ self.upstream_url = f"stdio://{command[0]}" # For display
143
+ logger.info(f"Using stdio transport: {' '.join(command)}")
144
+ elif upstream_url:
145
+ self._transport = "http"
146
+ self.upstream_url = upstream_url
147
+ self.upstream_auth = UpstreamAuth(**(upstream_auth or {}))
148
+ logger.info(f"Using HTTP transport: {upstream_url}")
149
+ else:
150
+ raise ValueError("Must provide either upstream_url (HTTP) or command (stdio)")
151
+
152
+ # State
153
+ self.tool_schemas: Dict[str, Dict[str, Any]] = {}
154
+ self._connected = False
155
+
156
+ # Connect to upstream and discover tools
157
+ self._connect_and_discover()
158
+
159
+ # Setup MACAWClient with discovered tools
160
+ self._setup_macaw_client(iam_token, user_name)
161
+
162
+ def _connect_and_discover(self):
163
+ """Connect to upstream MCP server and discover tools."""
164
+ try:
165
+ if self._transport == "stdio":
166
+ asyncio.run(self._async_discover_tools_stdio())
167
+ else:
168
+ asyncio.run(self._async_discover_tools_http())
169
+ except Exception as e:
170
+ logger.error(f"Failed to connect to upstream: {e}")
171
+ raise ConnectionError(f"Cannot connect to upstream MCP server: {e}")
172
+
173
+ async def _async_discover_tools_http(self):
174
+ """Discover tools from upstream MCP server via HTTP."""
175
+ try:
176
+ from mcp import ClientSession
177
+ from mcp.client.streamable_http import streamable_http_client
178
+ except ImportError:
179
+ raise ImportError(
180
+ "MCP client not installed. Install with: pip install macaw-adapters[mcp-proxy]"
181
+ )
182
+
183
+ # Build HTTP client with auth
184
+ http_client = self._create_http_client()
185
+
186
+ try:
187
+ # mcp<2 yields (read, write, get_session_id); mcp>=2 yields (read, write)
188
+ # because protocol sessions were removed in 2026-07-28. Index instead of
189
+ # unpacking so one call site works on both - the session id getter was
190
+ # already discarded.
191
+ async with streamable_http_client(
192
+ self.upstream_url,
193
+ http_client=http_client
194
+ ) as streams:
195
+ read_stream, write_stream = streams[0], streams[1]
196
+ async with ClientSession(read_stream, write_stream) as session:
197
+ await session.initialize()
198
+
199
+ # Discover tools
200
+ response = await session.list_tools()
201
+
202
+ for tool in response.tools:
203
+ self.tool_schemas[tool.name] = {
204
+ "name": tool.name,
205
+ "description": tool.description or f"Tool: {tool.name}",
206
+ "schema": _tool_schema(tool)
207
+ }
208
+
209
+ self._connected = True
210
+ logger.info(f"Discovered {len(self.tool_schemas)} tools from {self.upstream_url}")
211
+
212
+ finally:
213
+ if http_client:
214
+ await http_client.aclose()
215
+
216
+ async def _async_discover_tools_stdio(self):
217
+ """Discover tools from upstream MCP server via stdio subprocess."""
218
+ try:
219
+ from mcp import ClientSession
220
+ from mcp.client.stdio import stdio_client, StdioServerParameters
221
+ except ImportError:
222
+ raise ImportError(
223
+ "MCP client not installed. Install with: pip install macaw-adapters[mcp-proxy]"
224
+ )
225
+
226
+ params = StdioServerParameters(
227
+ command=self._stdio_command[0],
228
+ args=self._stdio_command[1:] if len(self._stdio_command) > 1 else [],
229
+ env=self._stdio_env or None
230
+ )
231
+
232
+ async with stdio_client(params) as (read_stream, write_stream):
233
+ async with ClientSession(read_stream, write_stream) as session:
234
+ await session.initialize()
235
+
236
+ # Discover tools
237
+ response = await session.list_tools()
238
+
239
+ for tool in response.tools:
240
+ self.tool_schemas[tool.name] = {
241
+ "name": tool.name,
242
+ "description": tool.description or f"Tool: {tool.name}",
243
+ "schema": _tool_schema(tool)
244
+ }
245
+
246
+ self._connected = True
247
+ logger.info(f"Discovered {len(self.tool_schemas)} tools via stdio")
248
+
249
+ def _create_http_client(self):
250
+ """Create httpx client with authentication headers.
251
+
252
+ mcp>=2 is built on httpx2 and type-checks the client it is handed, so the
253
+ client has to come from the same library the installed SDK uses.
254
+ """
255
+ try:
256
+ try:
257
+ import httpx2 as httpx # mcp>=2
258
+ except ImportError:
259
+ import httpx # mcp<2
260
+ except ImportError:
261
+ raise ImportError(
262
+ "httpx not installed. Install with: pip install macaw-adapters[mcp-proxy]"
263
+ )
264
+
265
+ headers = {}
266
+
267
+ if self.upstream_auth.type == "bearer" and self.upstream_auth.token:
268
+ headers["Authorization"] = f"Bearer {self.upstream_auth.token}"
269
+ elif self.upstream_auth.type == "api_key" and self.upstream_auth.api_key:
270
+ header_name = self.upstream_auth.header_name or "X-API-Key"
271
+ headers[header_name] = self.upstream_auth.api_key
272
+
273
+ # Always return a client, even without auth headers: httpx defaults to a
274
+ # 5s read timeout, which any upstream tool that calls an LLM will exceed.
275
+ # Letting the MCP SDK build its own client would reinstate that default.
276
+ return httpx.AsyncClient(
277
+ headers=headers or None,
278
+ timeout=httpx.Timeout(connect=30, read=300, write=30, pool=30),
279
+ )
280
+
281
+ def _setup_macaw_client(self, iam_token: Optional[str], user_name: Optional[str]):
282
+ """
283
+ Create MACAWClient with discovered tools as proxy handlers.
284
+
285
+ Key security property: MACAWClient creates AuthenticatedContext for
286
+ EACH invocation, providing per-call identity, policy, and signing.
287
+ This solves the "shared context" problem even with persistent stdio.
288
+ """
289
+ try:
290
+ from macaw_client import MACAWClient
291
+ except ImportError:
292
+ raise ImportError(
293
+ "MACAWClient not installed. Download from https://console.macawsecurity.ai"
294
+ )
295
+
296
+ # Build tool config with proxy handlers
297
+ # Use MAPL-compliant names: tool:{app_name}/{tool_name}
298
+ tools_config = {}
299
+ tool_names = []
300
+
301
+ for name, schema in self.tool_schemas.items():
302
+ mapl_name = f"tool:{self.app_name}/{name}"
303
+ tool_names.append(mapl_name)
304
+ tools_config[mapl_name] = {
305
+ "handler": self._make_proxy_handler(name),
306
+ "description": schema.get("description", ""),
307
+ "metadata": {
308
+ "schema": schema.get("schema", {}),
309
+ "upstream": self.upstream_url,
310
+ "transport": self._transport,
311
+ "proxy": True
312
+ }
313
+ }
314
+
315
+ # Build intent policy
316
+ full_intent_policy = {
317
+ "provided_capabilities": [{
318
+ "type": "tools",
319
+ "tools": tool_names,
320
+ "description": f"Proxied tools from {self.upstream_url} via {self._transport}"
321
+ }],
322
+ "description": f"SecureMCPProxy: {self.app_name} [{self._transport}] -> {self.upstream_url}",
323
+ **self.intent_policy
324
+ }
325
+
326
+ # Create MACAWClient
327
+ self.macaw_client = MACAWClient(
328
+ app_name=self.app_name,
329
+ app_version="1.0.0",
330
+ iam_token=iam_token,
331
+ user_name=user_name,
332
+ intent_policy=full_intent_policy,
333
+ tools=tools_config
334
+ )
335
+
336
+ # Register with LocalAgent
337
+ if not self.macaw_client.register():
338
+ raise RuntimeError("Failed to register with MACAW LocalAgent")
339
+
340
+ self.agent_id = self.macaw_client.agent_id
341
+ self.server_id = self.agent_id # Alias for consistency with other adapters
342
+
343
+ logger.info(f"SecureMCPProxy registered: {self.agent_id}")
344
+ logger.info(f" Tools: {list(self.tool_schemas.keys())}")
345
+
346
+ def _make_proxy_handler(self, tool_name: str):
347
+ """Create a handler that proxies calls to upstream MCP server."""
348
+ def handler(params: Dict[str, Any]) -> Any:
349
+ # This executes AFTER MACAWClient enforces policy
350
+ # MACAW has already created AuthenticatedContext for this call
351
+ if self._transport == "stdio":
352
+ result = asyncio.run(self._call_upstream_stdio(tool_name, params))
353
+ else:
354
+ result = asyncio.run(self._call_upstream_http(tool_name, params))
355
+
356
+ # Wrap result in dict format expected by MACAWClient
357
+ if not isinstance(result, dict):
358
+ result = {"result": result}
359
+ return result
360
+ return handler
361
+
362
+ async def _call_upstream_http(self, tool_name: str, params: Dict[str, Any]) -> Any:
363
+ """Call a tool on the upstream MCP server via HTTP."""
364
+ try:
365
+ from mcp import ClientSession
366
+ from mcp.client.streamable_http import streamable_http_client
367
+ except ImportError:
368
+ raise ImportError("MCP client not available")
369
+
370
+ http_client = self._create_http_client()
371
+
372
+ try:
373
+ # See _async_discover_tools_http() for why this is indexed, not unpacked.
374
+ async with streamable_http_client(
375
+ self.upstream_url,
376
+ http_client=http_client
377
+ ) as streams:
378
+ read_stream, write_stream = streams[0], streams[1]
379
+ async with ClientSession(read_stream, write_stream) as session:
380
+ await session.initialize()
381
+ result = await session.call_tool(tool_name, params)
382
+ return self._extract_result(result)
383
+
384
+ finally:
385
+ if http_client:
386
+ await http_client.aclose()
387
+
388
+ async def _call_upstream_stdio(self, tool_name: str, params: Dict[str, Any]) -> Any:
389
+ """Call a tool on the upstream MCP server via stdio subprocess."""
390
+ try:
391
+ from mcp import ClientSession
392
+ from mcp.client.stdio import stdio_client, StdioServerParameters
393
+ except ImportError:
394
+ raise ImportError("MCP client not available")
395
+
396
+ params_obj = StdioServerParameters(
397
+ command=self._stdio_command[0],
398
+ args=self._stdio_command[1:] if len(self._stdio_command) > 1 else [],
399
+ env=self._stdio_env or None
400
+ )
401
+
402
+ async with stdio_client(params_obj) as (read_stream, write_stream):
403
+ async with ClientSession(read_stream, write_stream) as session:
404
+ await session.initialize()
405
+ result = await session.call_tool(tool_name, params)
406
+ return self._extract_result(result)
407
+
408
+ def _extract_result(self, result: Any) -> Any:
409
+ """Extract content from MCP result format."""
410
+ if hasattr(result, 'content'):
411
+ # Handle MCP result format
412
+ if isinstance(result.content, list) and len(result.content) > 0:
413
+ content = result.content[0]
414
+ if hasattr(content, 'text'):
415
+ return content.text
416
+ return content
417
+ return result.content
418
+ return result
419
+
420
+ # =========================================================================
421
+ # PUBLIC API
422
+ # =========================================================================
423
+
424
+ def call_tool(self, tool_name: str, params: Optional[Dict[str, Any]] = None) -> Any:
425
+ """
426
+ Call a tool through MACAW security.
427
+
428
+ Flow:
429
+ 1. MACAWClient.invoke_tool() checks MAPL policy
430
+ 2. Signs the invocation
431
+ 3. Logs to audit
432
+ 4. Proxy handler forwards to upstream MCP server
433
+
434
+ Args:
435
+ tool_name: Name of the tool (e.g., "query_accounts")
436
+ params: Tool parameters
437
+
438
+ Returns:
439
+ Tool execution result from upstream
440
+
441
+ Raises:
442
+ PermissionError: If policy denies access
443
+ ConnectionError: If upstream is unreachable
444
+ """
445
+ if tool_name not in self.tool_schemas:
446
+ available = list(self.tool_schemas.keys())
447
+ raise ValueError(f"Unknown tool: {tool_name}. Available: {available}")
448
+
449
+ # Use MAPL-compliant name: tool:{app_name}/{tool_name}
450
+ mapl_name = f"tool:{self.app_name}/{tool_name}"
451
+
452
+ # Invoke through MACAW (goes through PEP)
453
+ return self.macaw_client.invoke_tool(
454
+ tool_name=mapl_name,
455
+ parameters=params or {},
456
+ target_agent=self.agent_id
457
+ )
458
+
459
+ def list_tools(self) -> List[Dict[str, Any]]:
460
+ """
461
+ List available tools from upstream MCP server.
462
+
463
+ Returns:
464
+ List of tool schemas with name, description, and input schema
465
+ """
466
+ return list(self.tool_schemas.values())
467
+
468
+ def get_tool_schema(self, tool_name: str) -> Optional[Dict[str, Any]]:
469
+ """
470
+ Get schema for a specific tool.
471
+
472
+ Args:
473
+ tool_name: Name of the tool
474
+
475
+ Returns:
476
+ Tool schema or None if not found
477
+ """
478
+ return self.tool_schemas.get(tool_name)
479
+
480
+ def bind_to_user(self, user_client) -> "BoundMCPProxy":
481
+ """
482
+ Bind proxy to a specific user's identity.
483
+
484
+ For multi-tenant SaaS apps where the service maintains the upstream
485
+ connection but each request runs with user's identity and policy.
486
+
487
+ Args:
488
+ user_client: MACAWClient instance with user's identity
489
+
490
+ Returns:
491
+ BoundMCPProxy that routes calls through user's identity
492
+
493
+ Example:
494
+ # Service creates proxy once
495
+ proxy = SecureMCPProxy(app_name="salesforce", ...)
496
+
497
+ # Per-request: bind to user
498
+ user_proxy = proxy.bind_to_user(user_macaw_client)
499
+ result = user_proxy.call_tool("query", {...}) # Alice's policy applies
500
+ """
501
+ return BoundMCPProxy(self, user_client)
502
+
503
+ def run(self, transport: str = "stdio", port: int = 8080):
504
+ """
505
+ Serve the proxied upstream tools to native (non-MACAW) MCP clients.
506
+
507
+ Turns this proxy into a drop-in replacement for the upstream server: point
508
+ Claude Desktop / Cursor at this process instead of the real one and every
509
+ tool call is policy-checked, signed, and audited on the way through.
510
+
511
+ proxy = SecureMCPProxy(app_name="databricks", command=["uvx", "..."])
512
+ proxy.run() # stdio
513
+ proxy.run(transport="http", port=8080) # http at /mcp
514
+
515
+ Args:
516
+ transport: "stdio" or "http" (how external clients reach this proxy;
517
+ independent of the transport used to reach upstream)
518
+ port: Bind port for transport="http"
519
+ """
520
+ from ._serve import serve
521
+
522
+ asyncio.run(
523
+ serve(
524
+ self.app_name,
525
+ "1.0.0",
526
+ registry=self.macaw_client,
527
+ target_agent=self.agent_id,
528
+ prefix=f"tool:{self.app_name}/",
529
+ transport=transport,
530
+ port=port,
531
+ )
532
+ )
533
+
534
+ def refresh_tools(self):
535
+ """
536
+ Re-discover tools from upstream MCP server.
537
+
538
+ Call this if upstream server's tools may have changed.
539
+ """
540
+ old_tools = set(self.tool_schemas.keys())
541
+ self._connect_and_discover()
542
+ new_tools = set(self.tool_schemas.keys())
543
+
544
+ added = new_tools - old_tools
545
+ removed = old_tools - new_tools
546
+
547
+ if added:
548
+ logger.info(f"New tools discovered: {added}")
549
+ # Register new tools with MACAWClient
550
+ for name in added:
551
+ schema = self.tool_schemas[name]
552
+ self.macaw_client.register_tool(
553
+ name,
554
+ handler=self._make_proxy_handler(name),
555
+ metadata={"schema": schema.get("schema", {})}
556
+ )
557
+
558
+ if removed:
559
+ logger.info(f"Tools no longer available: {removed}")
560
+
561
+ @property
562
+ def is_connected(self) -> bool:
563
+ """Check if connected to upstream MCP server."""
564
+ return self._connected
565
+
566
+ def __repr__(self) -> str:
567
+ tool_count = len(self.tool_schemas)
568
+ transport = self._transport.upper()
569
+ return f"<SecureMCPProxy {self.app_name} [{transport}] -> {self.upstream_url} ({tool_count} tools)>"
570
+
571
+
572
+ class BoundMCPProxy:
573
+ """
574
+ MCP Proxy bound to a specific user's identity.
575
+
576
+ All tool calls route through the user's MACAWClient,
577
+ applying their policies and logging under their identity.
578
+ """
579
+
580
+ def __init__(self, proxy: SecureMCPProxy, user_client):
581
+ """
582
+ Initialize bound proxy.
583
+
584
+ Args:
585
+ proxy: The SecureMCPProxy instance
586
+ user_client: MACAWClient with user's identity
587
+ """
588
+ self.proxy = proxy
589
+ self.user_client = user_client
590
+
591
+ def call_tool(self, tool_name: str, params: Optional[Dict[str, Any]] = None) -> Any:
592
+ """
593
+ Call a tool with user's identity and policy context.
594
+
595
+ Args:
596
+ tool_name: Name of the tool
597
+ params: Tool parameters
598
+
599
+ Returns:
600
+ Tool execution result
601
+ """
602
+ if tool_name not in self.proxy.tool_schemas:
603
+ available = list(self.proxy.tool_schemas.keys())
604
+ raise ValueError(f"Unknown tool: {tool_name}. Available: {available}")
605
+
606
+ # Use MAPL-compliant name: tool:{app_name}/{tool_name}
607
+ mapl_name = f"tool:{self.proxy.app_name}/{tool_name}"
608
+
609
+ # Invoke through user's MACAWClient (their policy applies)
610
+ return self.user_client.invoke_tool(
611
+ tool_name=mapl_name,
612
+ parameters=params or {},
613
+ target_agent=self.proxy.agent_id
614
+ )
615
+
616
+ def list_tools(self) -> List[Dict[str, Any]]:
617
+ """List available tools."""
618
+ return self.proxy.list_tools()
619
+
620
+ def get_tool_schema(self, tool_name: str) -> Optional[Dict[str, Any]]:
621
+ """Get schema for a specific tool."""
622
+ return self.proxy.get_tool_schema(tool_name)
623
+
624
+ def __repr__(self) -> str:
625
+ user_id = getattr(self.user_client, 'agent_id', 'unknown')
626
+ return f"<BoundMCPProxy {self.proxy.app_name} as {user_id}>"