@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,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)
|