@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,721 @@
1
+ """
2
+ SecureMCP Client: Transparent security for MCP clients
3
+
4
+ Simplified Architecture:
5
+ - Automatically registers with MACAW LocalAgent via MACAWClient
6
+ - All security handled by MACAWClient -> LocalAgent -> ToolAgent
7
+ - No duplicate permission checks - trust the MACAW protocol
8
+ - Supports MCP Sampling (server->client LLM requests)
9
+ """
10
+
11
+ import asyncio
12
+ import logging
13
+ from typing import Optional, Dict, Any, List, Callable
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Client:
19
+ """
20
+ SecureMCP Client with automatic registration and security.
21
+
22
+ All security is handled by MACAWClient -> LocalAgent -> ToolAgent.
23
+ This is a thin wrapper providing MCP-style API over MACAWClient.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ name: str,
29
+ version: str = "1.0.0",
30
+ server_url: Optional[str] = None,
31
+ **kwargs
32
+ ):
33
+ """
34
+ Initialize SecureMCP client.
35
+
36
+ Args:
37
+ name: Client name/application name
38
+ version: Client version
39
+ server_url: URL of the MCP server to connect to
40
+ **kwargs: Additional configuration
41
+ """
42
+ self.name = name
43
+ self.version = version
44
+ self.server_url = server_url
45
+ self.config = kwargs
46
+
47
+ # Connection state
48
+ self.connected = False
49
+ self.default_server = None
50
+
51
+ # Sampling handler for MCP server->client LLM requests
52
+ self._sampling_handler: Optional[Callable] = None
53
+
54
+ # Elicitation handler for MCP server->user input requests
55
+ self._elicitation_handler: Optional[Callable] = None
56
+
57
+ # Initialize MACAW client - handles all security
58
+ self.macaw_client = None
59
+ self._register_with_macaw()
60
+
61
+ def _register_with_macaw(self):
62
+ """Register with MACAW Local Agent via MACAWClient."""
63
+ try:
64
+ from macaw_client import MACAWClient
65
+
66
+ # Create MACAWClient - it handles all security
67
+ self.macaw_client = MACAWClient(
68
+ app_name=f"securemcp-client-{self.name}",
69
+ app_version=self.version,
70
+ intent_policy={
71
+ "required_resources": [
72
+ {
73
+ "resource": "tool/*",
74
+ "operations": ["invoke"],
75
+ "justification": f"SecureMCP client {self.name} needs to invoke tools"
76
+ }
77
+ ],
78
+ "data_access": ["none"],
79
+ "description": f"SecureMCP client: {self.name}"
80
+ }
81
+ )
82
+
83
+ # Register with Local Agent
84
+ if self.macaw_client.register():
85
+ logger.info(f"✅ Client registered: {self.macaw_client.agent_id}")
86
+ logger.info("All security handled by MACAWClient -> LocalAgent")
87
+ else:
88
+ raise RuntimeError("Failed to register with MACAW Local Agent")
89
+
90
+ except ImportError as e:
91
+ raise RuntimeError(f"MACAWClient not available: {e}")
92
+ except Exception as e:
93
+ raise RuntimeError(f"MACAW registration failed: {e}")
94
+
95
+ async def connect(self, server_url: Optional[str] = None, transport: str = "sse") -> bool:
96
+ """
97
+ Connect to an MCP server.
98
+
99
+ Note: For MACAW-based routing, explicit connection is not required.
100
+ Tools are invoked via LocalAgent routing using target_server parameter.
101
+
102
+ Args:
103
+ server_url: Server URL or "stdio" for STDIO transport
104
+ transport: Transport type ("sse" or "stdio")
105
+
106
+ Returns:
107
+ True if connection successful
108
+ """
109
+ # Handle STDIO transport (direct connection without LocalAgent)
110
+ if server_url == "stdio" or transport == "stdio":
111
+ from .transport.stdio import StdioClient
112
+
113
+ logger.info("Connecting via STDIO transport")
114
+ self.stdio_client = StdioClient(self)
115
+
116
+ try:
117
+ capabilities = await self.stdio_client.initialize()
118
+ self.connected = True
119
+ logger.info(f"Connected via STDIO: {capabilities.get('serverInfo', {})}")
120
+ return True
121
+ except Exception as e:
122
+ logger.error(f"STDIO connection failed: {e}")
123
+ return False
124
+
125
+ # For MACAW routing, we don't need explicit connection
126
+ # Tools are invoked via LocalAgent using target_server
127
+ if server_url:
128
+ self.server_url = server_url
129
+ self.set_default_server(server_url)
130
+
131
+ self.connected = True
132
+ logger.info("Client ready for MACAW LocalAgent routing")
133
+ return True
134
+
135
+ async def call_tool(
136
+ self,
137
+ tool_name: str,
138
+ arguments: Dict[str, Any],
139
+ target_server: Optional[str] = None
140
+ ) -> Any:
141
+ """
142
+ Call a tool on the MCP server.
143
+
144
+ Per MCP specification: tools are discovered and invoked within
145
+ the context of a specific server connection. Security is handled
146
+ by MACAWClient -> LocalAgent -> ToolAgent.
147
+
148
+ Args:
149
+ tool_name: Name of the tool to call
150
+ arguments: Tool arguments
151
+ target_server: Target server/agent (e.g., "local:mohan/app:securemcp-calc:abc123")
152
+
153
+ Returns:
154
+ Tool execution result
155
+ """
156
+ # Handle STDIO transport
157
+ if hasattr(self, 'stdio_client'):
158
+ return await self.stdio_client.call_tool(tool_name, arguments)
159
+
160
+ # Determine target server
161
+ target = target_server or self.default_server
162
+ if not target:
163
+ raise ValueError("target_server required (per MCP spec)")
164
+
165
+ # Use MACAWClient for tool invocation - it handles all security
166
+ logger.info(f"Invoking tool '{tool_name}' on '{target}'")
167
+ result = self.macaw_client.invoke_tool(
168
+ tool_name,
169
+ parameters=arguments,
170
+ target_agent=target
171
+ )
172
+ logger.info(f"Tool '{tool_name}' completed: {result}")
173
+ return result
174
+
175
+ async def get_resource(
176
+ self,
177
+ resource_uri: str,
178
+ target_server: Optional[str] = None
179
+ ) -> Any:
180
+ """
181
+ Get a resource from the MCP server (read-only tool semantics).
182
+
183
+ Resources are registered as tools with "resource:" prefix.
184
+ Security is handled by MACAWClient -> LocalAgent -> ToolAgent.
185
+
186
+ Args:
187
+ resource_uri: Resource URI (e.g., "calc://history")
188
+ target_server: Target server providing the resource
189
+
190
+ Returns:
191
+ Resource content
192
+ """
193
+ target = target_server or self.default_server
194
+ if not target:
195
+ raise ValueError("target_server required for resource access")
196
+
197
+ # Resources are registered as "resource:{uri}" tools
198
+ return self.macaw_client.invoke_tool(
199
+ f"resource:{resource_uri}",
200
+ parameters={},
201
+ target_agent=target
202
+ )
203
+
204
+ async def get_prompt(
205
+ self,
206
+ prompt_name: str,
207
+ arguments: Optional[Dict[str, Any]] = None,
208
+ target_server: Optional[str] = None
209
+ ) -> str:
210
+ """
211
+ Get a prompt template from the MCP server.
212
+
213
+ Prompts are registered as tools with "prompt:" prefix.
214
+ Use MACAWClient.create_authenticated_prompt() for secure prompts.
215
+
216
+ Args:
217
+ prompt_name: Name of the prompt
218
+ arguments: Optional prompt arguments
219
+ target_server: Target server providing the prompt
220
+
221
+ Returns:
222
+ Prompt template text
223
+ """
224
+ target = target_server or self.default_server
225
+ if not target:
226
+ raise ValueError("target_server required for prompt access")
227
+
228
+ # Prompts are registered as "prompt:{name}" tools
229
+ return self.macaw_client.invoke_tool(
230
+ f"prompt:{prompt_name}",
231
+ parameters=arguments or {},
232
+ target_agent=target
233
+ )
234
+
235
+ async def list_servers(self) -> List[Dict[str, Any]]:
236
+ """
237
+ Discover available MCP servers registered with Local Agent.
238
+
239
+ Returns:
240
+ List of server information including:
241
+ - name: Server name (e.g., "app/securemcp-calculator")
242
+ - instance_id: Full instance ID
243
+ - tools: List of tools provided
244
+ - metadata: Additional server metadata
245
+ """
246
+ try:
247
+ # Use public MACAWClient API for agent discovery
248
+ agents = self.macaw_client.list_agents(agent_type="app")
249
+
250
+ servers = []
251
+ for agent in agents:
252
+ agent_id = agent.get("agent_id", "")
253
+
254
+ # Get full details to retrieve tools
255
+ details = self.macaw_client.get_agent_info(agent_id)
256
+ if not details:
257
+ continue
258
+
259
+ tools = details.get("tools", {})
260
+ if not tools:
261
+ continue # Only include servers that provide tools
262
+
263
+ # Tools is a dict, get the keys as tool names
264
+ tool_list = list(tools.keys()) if isinstance(tools, dict) else tools
265
+
266
+ server_info = {
267
+ "name": agent_id.split(":")[0] if ":" in agent_id else agent_id,
268
+ "instance_id": agent_id,
269
+ "tools": tool_list,
270
+ "metadata": details.get("metadata", {})
271
+ }
272
+ servers.append(server_info)
273
+
274
+ return servers
275
+
276
+ except Exception as e:
277
+ logger.error(f"Error listing servers: {e}")
278
+ return []
279
+
280
+ async def list_tools(self, server_name: str = None) -> List[Dict[str, Any]]:
281
+ """
282
+ List tools available from a specific server or all servers.
283
+
284
+ Tools are registered as ToolAgents with pattern: {server_id}/tool.{name}
285
+ Filters out resources (resource:*) and prompts (prompt:*).
286
+
287
+ Args:
288
+ server_name: Optional server name filter (e.g., "securemcp-calculator")
289
+ If None, returns tools from all servers
290
+
291
+ Returns:
292
+ List of tool information including:
293
+ - name: Tool name
294
+ - description: Tool description
295
+ - server: Server instance ID
296
+ """
297
+ tools = []
298
+
299
+ # Get all app agents - tools are registered as separate ToolAgents
300
+ agents = self.macaw_client.list_agents(agent_type="app")
301
+
302
+ for agent in agents:
303
+ agent_id = agent.get("agent_id", "")
304
+
305
+ # Only look at tool agents (pattern: .../tool.{name})
306
+ if "/tool." not in agent_id:
307
+ continue
308
+
309
+ # Filter by server name if specified
310
+ if server_name and server_name not in agent_id:
311
+ continue
312
+
313
+ # Extract tool name from agent_id
314
+ # Pattern: local:mohan/app:securemcp-calc:abc123/tool.add
315
+ tool_name = agent_id.split("/tool.")[-1]
316
+
317
+ # Skip resources and prompts
318
+ if tool_name.startswith("resource:") or tool_name.startswith("prompt:"):
319
+ continue
320
+
321
+ # Extract server ID (part before /tool.)
322
+ server_id = agent_id.rsplit("/tool.", 1)[0]
323
+
324
+ # Get description from agent info if available
325
+ details = self.macaw_client.get_agent_info(agent_id)
326
+ description = ""
327
+ if details and details.get("metadata"):
328
+ description = details["metadata"].get("description", "")
329
+
330
+ tool_info = {
331
+ "name": tool_name,
332
+ "description": description,
333
+ "server": server_id
334
+ }
335
+ tools.append(tool_info)
336
+
337
+ return tools
338
+
339
+ async def list_resources(self, server_name: str = None) -> List[Dict[str, Any]]:
340
+ """
341
+ List resources available from a specific server or all servers.
342
+
343
+ Resources are registered as ToolAgents with pattern: {server_id}/tool.resource:{uri}
344
+
345
+ Args:
346
+ server_name: Optional server name filter
347
+ If None, returns resources from all servers
348
+
349
+ Returns:
350
+ List of resource information including:
351
+ - uri: Resource URI pattern
352
+ - name: Resource name (derived from URI)
353
+ - description: Resource description
354
+ - server: Server instance ID
355
+ """
356
+ resources = []
357
+
358
+ agents = self.macaw_client.list_agents(agent_type="app")
359
+
360
+ for agent in agents:
361
+ agent_id = agent.get("agent_id", "")
362
+
363
+ # Only look at resource tool agents
364
+ if "/tool.resource:" not in agent_id:
365
+ continue
366
+
367
+ if server_name and server_name not in agent_id:
368
+ continue
369
+
370
+ # Extract URI from agent_id
371
+ # Pattern: .../tool.resource:calc://history
372
+ uri = agent_id.split("/tool.resource:")[-1]
373
+ server_id = agent_id.rsplit("/tool.resource:", 1)[0]
374
+
375
+ # Get description from agent info if available
376
+ details = self.macaw_client.get_agent_info(agent_id)
377
+ description = ""
378
+ if details and details.get("metadata"):
379
+ description = details["metadata"].get("description", "")
380
+
381
+ resource_info = {
382
+ "uri": uri,
383
+ "name": uri.split("://")[-1] if "://" in uri else uri,
384
+ "description": description,
385
+ "server": server_id
386
+ }
387
+ resources.append(resource_info)
388
+
389
+ return resources
390
+
391
+ async def list_prompts(self, server_name: str = None) -> List[Dict[str, Any]]:
392
+ """
393
+ List prompts available from a specific server or all servers.
394
+
395
+ Prompts are registered as ToolAgents with pattern: {server_id}/tool.prompt:{name}
396
+
397
+ Args:
398
+ server_name: Optional server name filter
399
+ If None, returns prompts from all servers
400
+
401
+ Returns:
402
+ List of prompt information including:
403
+ - name: Prompt name
404
+ - description: Prompt description
405
+ - server: Server instance ID
406
+ """
407
+ prompts = []
408
+
409
+ agents = self.macaw_client.list_agents(agent_type="app")
410
+
411
+ for agent in agents:
412
+ agent_id = agent.get("agent_id", "")
413
+
414
+ # Only look at prompt tool agents
415
+ if "/tool.prompt:" not in agent_id:
416
+ continue
417
+
418
+ if server_name and server_name not in agent_id:
419
+ continue
420
+
421
+ # Extract prompt name from agent_id
422
+ # Pattern: .../tool.prompt:calculation_prompt
423
+ name = agent_id.split("/tool.prompt:")[-1]
424
+ server_id = agent_id.rsplit("/tool.prompt:", 1)[0]
425
+
426
+ # Get description from agent info if available
427
+ details = self.macaw_client.get_agent_info(agent_id)
428
+ description = ""
429
+ if details and details.get("metadata"):
430
+ description = details["metadata"].get("description", "")
431
+
432
+ prompt_info = {
433
+ "name": name,
434
+ "description": description,
435
+ "server": server_id
436
+ }
437
+ prompts.append(prompt_info)
438
+
439
+ return prompts
440
+
441
+ async def list_roots(self, server_name: str = None) -> List[Dict[str, Any]]:
442
+ """
443
+ List filesystem roots available from a specific server or all servers.
444
+
445
+ Roots are declared via MAPL in the server's intent_policy.
446
+ They define which directories a server has access to.
447
+
448
+ Args:
449
+ server_name: Optional server name filter
450
+ If None, returns roots from all servers
451
+
452
+ Returns:
453
+ List of root information including:
454
+ - path: Filesystem path
455
+ - server: Server instance ID
456
+ - operations: Permitted operations (read, write, list)
457
+ """
458
+ roots = []
459
+
460
+ agents = self.macaw_client.list_agents(agent_type="app")
461
+
462
+ for agent in agents:
463
+ agent_id = agent.get("agent_id", "")
464
+
465
+ # Skip tool sub-agents
466
+ if "/tool." in agent_id:
467
+ continue
468
+
469
+ if server_name and server_name not in agent_id:
470
+ continue
471
+
472
+ # Get server details to find roots
473
+ details = self.macaw_client.get_agent_info(agent_id)
474
+ if not details:
475
+ continue
476
+
477
+ # Roots are in intent_policy or metadata
478
+ intent_policy = details.get("intent_policy", {})
479
+ server_roots = intent_policy.get("roots", [])
480
+
481
+ # Also check required_resources for file:// patterns
482
+ for resource in intent_policy.get("required_resources", []):
483
+ resource_pattern = resource.get("resource", "")
484
+ if resource_pattern.startswith("file://"):
485
+ # Extract path from file://path/*
486
+ path = resource_pattern[7:] # Remove "file://"
487
+ if path.endswith("/*"):
488
+ path = path[:-2] # Remove "/*"
489
+ if path not in server_roots:
490
+ server_roots.append(path)
491
+
492
+ for root_path in server_roots:
493
+ root_info = {
494
+ "path": root_path,
495
+ "server": agent_id,
496
+ "operations": ["read", "write", "list"] # Default operations
497
+ }
498
+ roots.append(root_info)
499
+
500
+ return roots
501
+
502
+ def set_default_server(self, server_name: str) -> None:
503
+ """
504
+ Set the default server for tool invocations.
505
+
506
+ This allows calling tools without specifying target_server each time.
507
+
508
+ Args:
509
+ server_name: Server name (e.g., "app/securemcp-calculator")
510
+ """
511
+ self.default_server = server_name
512
+ logger.info(f"Set default server to: {server_name}")
513
+
514
+ def set_sampling_handler(
515
+ self,
516
+ handler: Callable[[str, Optional[str], int, float, Dict[str, Any]], str]
517
+ ) -> None:
518
+ """
519
+ Set the handler for MCP Sampling requests (server->client LLM).
520
+
521
+ When a server tool calls ctx.sample(), this handler is invoked to
522
+ perform the LLM completion. This enables servers to leverage the
523
+ client's LLM capabilities.
524
+
525
+ Args:
526
+ handler: Async or sync function that performs LLM completion.
527
+ Signature: (prompt, system_prompt, max_tokens, temperature, **kwargs) -> str
528
+
529
+ Example:
530
+ async def my_llm_handler(prompt, system_prompt, max_tokens, temperature, **kwargs):
531
+ # Use your preferred LLM (OpenAI, Anthropic, etc.)
532
+ response = await openai.chat.completions.create(
533
+ model="gpt-4",
534
+ messages=[
535
+ {"role": "system", "content": system_prompt or "You are helpful."},
536
+ {"role": "user", "content": prompt}
537
+ ],
538
+ max_tokens=max_tokens,
539
+ temperature=temperature
540
+ )
541
+ return response.choices[0].message.content
542
+
543
+ client.set_sampling_handler(my_llm_handler)
544
+ """
545
+ self._sampling_handler = handler
546
+
547
+ # Register _mcp_sample tool to handle sampling requests from servers
548
+ def sample_tool_handler(params: Dict[str, Any]) -> Dict[str, Any]:
549
+ if not self._sampling_handler:
550
+ return {"error": "No sampling handler configured"}
551
+
552
+ prompt = params.get("prompt", "")
553
+ system_prompt = params.get("system_prompt")
554
+ max_tokens = params.get("max_tokens", 1000)
555
+ # None means the calling tool did not ask for a temperature. Do not
556
+ # invent one here - only the handler knows which model it is calling,
557
+ # and some models reject sampling parameters outright.
558
+ temperature = params.get("temperature")
559
+
560
+ # Remove known params, pass rest as kwargs
561
+ known_params = {"prompt", "system_prompt", "max_tokens", "temperature",
562
+ "request_id", "source_tool"}
563
+ extra_kwargs = {k: v for k, v in params.items() if k not in known_params}
564
+
565
+ try:
566
+ result = self._sampling_handler(
567
+ prompt, system_prompt, max_tokens, temperature, **extra_kwargs
568
+ )
569
+
570
+ # Handle async handlers
571
+ if asyncio.iscoroutine(result):
572
+ loop = asyncio.get_event_loop()
573
+ if loop.is_running():
574
+ import concurrent.futures
575
+ with concurrent.futures.ThreadPoolExecutor() as pool:
576
+ result = pool.submit(asyncio.run, result).result()
577
+ else:
578
+ result = loop.run_until_complete(result)
579
+
580
+ return {"response": result}
581
+
582
+ except Exception as e:
583
+ logger.error(f"Sampling handler error: {e}")
584
+ return {"error": str(e)}
585
+
586
+ # Register the sampling tool with MACAWClient using public API
587
+ if self.macaw_client:
588
+ self.macaw_client.register_tool("_mcp_sample", sample_tool_handler)
589
+ logger.info("Sampling handler registered - servers can now use ctx.sample()")
590
+
591
+ def set_elicitation_handler(
592
+ self,
593
+ handler: Callable[[str, Optional[List[str]], str, Optional[str], bool, Dict[str, Any]], Any]
594
+ ) -> None:
595
+ """
596
+ Set the handler for MCP Elicitation requests (server->user input).
597
+
598
+ When a server tool calls ctx.elicit(), this handler is invoked to
599
+ prompt the user and return their input.
600
+
601
+ Args:
602
+ handler: Function that prompts the user and returns their input.
603
+ Signature: (prompt, options, input_type, default, required, **kwargs) -> Any
604
+
605
+ Example:
606
+ def my_input_handler(prompt, options, input_type, default, required, **kwargs):
607
+ if input_type == "confirm":
608
+ response = input(f"{prompt} (y/n): ")
609
+ return response.lower() in ("y", "yes")
610
+ elif input_type == "select" and options:
611
+ print(prompt)
612
+ for i, opt in enumerate(options):
613
+ print(f" {i+1}. {opt}")
614
+ choice = int(input("Enter number: ")) - 1
615
+ return options[choice]
616
+ else:
617
+ return input(f"{prompt}: ") or default
618
+
619
+ client.set_elicitation_handler(my_input_handler)
620
+ """
621
+ self._elicitation_handler = handler
622
+
623
+ # Register _mcp_elicit tool to handle elicitation requests from servers
624
+ def elicit_tool_handler(params: Dict[str, Any]) -> Dict[str, Any]:
625
+ if not self._elicitation_handler:
626
+ return {"error": "No elicitation handler configured"}
627
+
628
+ prompt = params.get("prompt", "")
629
+ options = params.get("options")
630
+ input_type = params.get("input_type", "text")
631
+ default = params.get("default")
632
+ required = params.get("required", True)
633
+
634
+ # Remove known params, pass rest as kwargs
635
+ known_params = {"prompt", "options", "input_type", "default", "required",
636
+ "request_id", "source_tool"}
637
+ extra_kwargs = {k: v for k, v in params.items() if k not in known_params}
638
+
639
+ try:
640
+ result = self._elicitation_handler(
641
+ prompt, options, input_type, default, required, **extra_kwargs
642
+ )
643
+
644
+ # Handle async handlers
645
+ if asyncio.iscoroutine(result):
646
+ loop = asyncio.get_event_loop()
647
+ if loop.is_running():
648
+ import concurrent.futures
649
+ with concurrent.futures.ThreadPoolExecutor() as pool:
650
+ result = pool.submit(asyncio.run, result).result()
651
+ else:
652
+ result = loop.run_until_complete(result)
653
+
654
+ return {"response": result}
655
+
656
+ except Exception as e:
657
+ logger.error(f"Elicitation handler error: {e}")
658
+ return {"error": str(e)}
659
+
660
+ # Register the elicitation tool with MACAWClient using public API
661
+ if self.macaw_client:
662
+ self.macaw_client.register_tool("_mcp_elicit", elicit_tool_handler)
663
+ logger.info("Elicitation handler registered - servers can now use ctx.elicit()")
664
+
665
+ async def disconnect(self):
666
+ """Disconnect from the MCP server"""
667
+ if self.macaw_client:
668
+ try:
669
+ self.macaw_client.unregister()
670
+ logger.info("Disconnected from MACAW Local Agent")
671
+ except Exception as e:
672
+ logger.warning(f"Error during disconnect: {e}")
673
+ self.connected = False
674
+
675
+ def get_identity(self) -> Dict[str, Any]:
676
+ """Get the client's identity information from MACAWClient."""
677
+ if self.macaw_client:
678
+ return {
679
+ 'client_id': self.macaw_client.agent_id,
680
+ 'public_key': self.macaw_client.public_key.hex() if self.macaw_client.public_key else None,
681
+ 'metadata': {'name': self.name, 'version': self.version}
682
+ }
683
+ return {'client_id': 'unknown', 'metadata': {'error': 'Not registered'}}
684
+
685
+ @property
686
+ def is_secure(self) -> bool:
687
+ """Check if client has security features enabled"""
688
+ return self.macaw_client is not None
689
+
690
+ @property
691
+ def client_id(self) -> str:
692
+ """Get the client's unique ID from MACAWClient"""
693
+ return self.macaw_client.agent_id if self.macaw_client else "unknown"
694
+
695
+ def __repr__(self) -> str:
696
+ """String representation"""
697
+ status = "secure" if self.is_secure else "not-registered"
698
+ return f"<SecureMCPClient {self.name} ({self.client_id}) [{status}]>"
699
+
700
+
701
+ # Convenience function for creating clients
702
+ def create_client(
703
+ name: str,
704
+ server_url: Optional[str] = None,
705
+ **kwargs
706
+ ) -> Client:
707
+ """
708
+ Create a SecureMCP client with automatic registration.
709
+
710
+ This is the recommended way to create clients as it ensures
711
+ proper registration and setup.
712
+
713
+ Args:
714
+ name: Client name
715
+ server_url: Optional server URL
716
+ **kwargs: Additional configuration
717
+
718
+ Returns:
719
+ Configured Client instance
720
+ """
721
+ return Client(name=name, server_url=server_url, **kwargs)