@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,785 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MACAW SecureMCP - FastMCP-compatible API with MACAW Security.
|
|
3
|
+
|
|
4
|
+
Provides the same decorator-based API as FastMCP while routing all tool
|
|
5
|
+
invocations through the MACAW security layer for policy enforcement,
|
|
6
|
+
cryptographic signing, and audit logging.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from macaw_adapters.mcp import SecureMCP, Context
|
|
10
|
+
|
|
11
|
+
mcp = SecureMCP("calculator")
|
|
12
|
+
|
|
13
|
+
@mcp.tool(description="Add two numbers")
|
|
14
|
+
def add(a: float, b: float) -> float:
|
|
15
|
+
return a + b
|
|
16
|
+
|
|
17
|
+
@mcp.resource("calc://history")
|
|
18
|
+
def get_history(ctx: Context) -> list:
|
|
19
|
+
return ctx.get("calc_history") or []
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
mcp.run()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import inspect
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
29
|
+
from functools import wraps
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _json_schema(parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
37
|
+
"""Convert _extract_parameters() output into a JSON Schema for MCP's inputSchema."""
|
|
38
|
+
return {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
name: {"type": info.get("type", "string")}
|
|
42
|
+
for name, info in parameters.items()
|
|
43
|
+
},
|
|
44
|
+
"required": [
|
|
45
|
+
name for name, info in parameters.items() if info.get("required")
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Context:
|
|
52
|
+
"""
|
|
53
|
+
Request context passed to tool handlers.
|
|
54
|
+
|
|
55
|
+
Provides access to context vault, progress reporting, and cross-tool calls.
|
|
56
|
+
Compatible with FastMCP's Context object.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
request_id: str = ""
|
|
60
|
+
tool_name: str = ""
|
|
61
|
+
source_agent: str = "" # Caller's agent ID (for sampling/elicitation callbacks)
|
|
62
|
+
_client: Any = None # MACAWClient reference
|
|
63
|
+
_metadata: Dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
def get(self, key: str) -> Any:
|
|
66
|
+
"""Get value from context vault."""
|
|
67
|
+
if self._client:
|
|
68
|
+
return self._client.context_get(key)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def set(self, key: str, value: Any) -> None:
|
|
72
|
+
"""Set value in context vault."""
|
|
73
|
+
if self._client:
|
|
74
|
+
self._client.context_set(key, value)
|
|
75
|
+
|
|
76
|
+
async def report_progress(self, progress: float, message: str = None) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Report execution progress (0.0 to 1.0).
|
|
79
|
+
|
|
80
|
+
This is async to support streaming progress updates.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
progress: Progress percentage from 0.0 to 1.0
|
|
84
|
+
message: Optional progress message
|
|
85
|
+
"""
|
|
86
|
+
if self._client:
|
|
87
|
+
self._client.log_event(
|
|
88
|
+
event_type="tool_progress",
|
|
89
|
+
action="progress",
|
|
90
|
+
target=self.tool_name,
|
|
91
|
+
metadata={
|
|
92
|
+
"request_id": self.request_id,
|
|
93
|
+
"progress": progress,
|
|
94
|
+
"message": message
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def read_resource(self, uri: str) -> Any:
|
|
99
|
+
"""
|
|
100
|
+
Read another resource (for resource dependencies).
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
uri: Resource URI to read
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Resource content
|
|
107
|
+
"""
|
|
108
|
+
if self._client:
|
|
109
|
+
return self._client.invoke_tool(f"resource:{uri}", {})
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
async def sample(
|
|
113
|
+
self,
|
|
114
|
+
prompt: str,
|
|
115
|
+
system_prompt: str = None,
|
|
116
|
+
max_tokens: int = 1000,
|
|
117
|
+
temperature: Optional[float] = None,
|
|
118
|
+
**kwargs
|
|
119
|
+
) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Request an LLM completion from the client (MCP Sampling).
|
|
122
|
+
|
|
123
|
+
This allows server tools to leverage the client's LLM capabilities
|
|
124
|
+
for AI-assisted processing within tool execution.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
prompt: The user prompt to send to the LLM
|
|
128
|
+
system_prompt: Optional system prompt
|
|
129
|
+
max_tokens: Maximum tokens in response
|
|
130
|
+
temperature: Sampling temperature. Left unset by default: this server
|
|
131
|
+
does not know which model the client will use, and sampling
|
|
132
|
+
parameters are rejected outright by some models. Set it only if
|
|
133
|
+
the caller has a reason to, and accept that a client may ignore
|
|
134
|
+
or reject it.
|
|
135
|
+
**kwargs: Additional LLM parameters
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The LLM's text response
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
summary = await ctx.sample(
|
|
142
|
+
prompt=f"Summarize this text: {long_text}",
|
|
143
|
+
system_prompt="You are a helpful summarization assistant.",
|
|
144
|
+
max_tokens=500
|
|
145
|
+
)
|
|
146
|
+
"""
|
|
147
|
+
if not self._client:
|
|
148
|
+
raise RuntimeError("No client available for sampling")
|
|
149
|
+
|
|
150
|
+
if not self.source_agent:
|
|
151
|
+
raise RuntimeError("No source_agent available - sampling requires caller context")
|
|
152
|
+
|
|
153
|
+
params = {
|
|
154
|
+
"prompt": prompt,
|
|
155
|
+
"system_prompt": system_prompt,
|
|
156
|
+
"max_tokens": max_tokens,
|
|
157
|
+
"request_id": self.request_id,
|
|
158
|
+
"source_tool": self.tool_name,
|
|
159
|
+
**kwargs
|
|
160
|
+
}
|
|
161
|
+
# Only send what the caller actually asked for. Inventing a temperature
|
|
162
|
+
# here means shipping a model-specific parameter to a model we know
|
|
163
|
+
# nothing about - which is the whole point of sampling.
|
|
164
|
+
if temperature is not None:
|
|
165
|
+
params["temperature"] = temperature
|
|
166
|
+
|
|
167
|
+
# Route sampling request back to the calling client's sampling handler
|
|
168
|
+
logger.info(f"[ctx.sample] Calling back to source_agent: {self.source_agent}")
|
|
169
|
+
result = self._client.invoke_tool(
|
|
170
|
+
"_mcp_sample",
|
|
171
|
+
params,
|
|
172
|
+
target_agent=self.source_agent # Call back to the original caller
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if isinstance(result, dict):
|
|
176
|
+
return result.get("response", result.get("result", str(result)))
|
|
177
|
+
return str(result)
|
|
178
|
+
|
|
179
|
+
async def elicit(
|
|
180
|
+
self,
|
|
181
|
+
prompt: str,
|
|
182
|
+
options: List[str] = None,
|
|
183
|
+
input_type: str = "text",
|
|
184
|
+
default: str = None,
|
|
185
|
+
required: bool = True,
|
|
186
|
+
**kwargs
|
|
187
|
+
) -> Any:
|
|
188
|
+
"""
|
|
189
|
+
Request user input from the client (MCP Elicitation).
|
|
190
|
+
|
|
191
|
+
This allows server tools to interactively request information
|
|
192
|
+
from the user during tool execution.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
prompt: The question or prompt to show the user
|
|
196
|
+
options: List of choices for selection input
|
|
197
|
+
input_type: Type of input ("text", "select", "confirm", "number")
|
|
198
|
+
default: Default value if user doesn't provide input
|
|
199
|
+
required: Whether input is required
|
|
200
|
+
**kwargs: Additional elicitation parameters
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
User's input (string, bool for confirm, or selected option)
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
# Simple text input
|
|
207
|
+
name = await ctx.elicit("What is your name?")
|
|
208
|
+
|
|
209
|
+
# Confirmation
|
|
210
|
+
proceed = await ctx.elicit(
|
|
211
|
+
"Delete this file?",
|
|
212
|
+
input_type="confirm",
|
|
213
|
+
default="no"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Selection from options
|
|
217
|
+
color = await ctx.elicit(
|
|
218
|
+
"Choose a color:",
|
|
219
|
+
options=["red", "green", "blue"],
|
|
220
|
+
input_type="select"
|
|
221
|
+
)
|
|
222
|
+
"""
|
|
223
|
+
if not self._client:
|
|
224
|
+
raise RuntimeError("No client available for elicitation")
|
|
225
|
+
|
|
226
|
+
if not self.source_agent:
|
|
227
|
+
raise RuntimeError("No source_agent available - elicitation requires caller context")
|
|
228
|
+
|
|
229
|
+
# Route elicitation request back to the calling client's handler
|
|
230
|
+
result = self._client.invoke_tool(
|
|
231
|
+
"_mcp_elicit",
|
|
232
|
+
{
|
|
233
|
+
"prompt": prompt,
|
|
234
|
+
"options": options,
|
|
235
|
+
"input_type": input_type,
|
|
236
|
+
"default": default,
|
|
237
|
+
"required": required,
|
|
238
|
+
"request_id": self.request_id,
|
|
239
|
+
"source_tool": self.tool_name,
|
|
240
|
+
**kwargs
|
|
241
|
+
},
|
|
242
|
+
target_agent=self.source_agent # Call back to the original caller
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if isinstance(result, dict):
|
|
246
|
+
response = result.get("response", result.get("result"))
|
|
247
|
+
|
|
248
|
+
# Convert response based on input_type
|
|
249
|
+
if input_type == "confirm":
|
|
250
|
+
if isinstance(response, bool):
|
|
251
|
+
return response
|
|
252
|
+
return str(response).lower() in ("yes", "y", "true", "1")
|
|
253
|
+
elif input_type == "number":
|
|
254
|
+
try:
|
|
255
|
+
return float(response) if "." in str(response) else int(response)
|
|
256
|
+
except (ValueError, TypeError):
|
|
257
|
+
return default
|
|
258
|
+
return response
|
|
259
|
+
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
# ========================================
|
|
263
|
+
# Logging methods (FastMCP-compatible)
|
|
264
|
+
# ========================================
|
|
265
|
+
|
|
266
|
+
def log(self, level: str, message: str, **kwargs) -> None:
|
|
267
|
+
"""
|
|
268
|
+
Log a message at the specified level.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
level: Log level (debug, info, warning, error)
|
|
272
|
+
message: Log message
|
|
273
|
+
**kwargs: Additional metadata
|
|
274
|
+
"""
|
|
275
|
+
if self._client:
|
|
276
|
+
self._client.log_event(
|
|
277
|
+
event_type="tool_log",
|
|
278
|
+
source=self.tool_name,
|
|
279
|
+
action=level,
|
|
280
|
+
target=self.request_id,
|
|
281
|
+
outcome="success", # Log events are informational, always "success"
|
|
282
|
+
metadata={"level": level, "message": message, **kwargs}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def debug(self, message: str, **kwargs) -> None:
|
|
286
|
+
"""Log a debug message."""
|
|
287
|
+
self.log("debug", message, **kwargs)
|
|
288
|
+
|
|
289
|
+
def info(self, message: str, **kwargs) -> None:
|
|
290
|
+
"""Log an info message."""
|
|
291
|
+
self.log("info", message, **kwargs)
|
|
292
|
+
|
|
293
|
+
def warning(self, message: str, **kwargs) -> None:
|
|
294
|
+
"""Log a warning message."""
|
|
295
|
+
self.log("warning", message, **kwargs)
|
|
296
|
+
|
|
297
|
+
def error(self, message: str, **kwargs) -> None:
|
|
298
|
+
"""Log an error message."""
|
|
299
|
+
self.log("error", message, **kwargs)
|
|
300
|
+
|
|
301
|
+
def audit(
|
|
302
|
+
self,
|
|
303
|
+
action: str,
|
|
304
|
+
target: str = None,
|
|
305
|
+
outcome: str = "success",
|
|
306
|
+
**metadata
|
|
307
|
+
) -> None:
|
|
308
|
+
"""
|
|
309
|
+
Create a cryptographically signed audit entry.
|
|
310
|
+
|
|
311
|
+
This is a MACAW-specific feature that creates tamper-evident audit logs
|
|
312
|
+
with cryptographic signatures for compliance and forensics.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
action: The action being audited (e.g., "data_access", "approval", "modification")
|
|
316
|
+
target: What the action affects (e.g., resource name, record ID)
|
|
317
|
+
outcome: Result of the action ("success", "failure", "denied")
|
|
318
|
+
**metadata: Additional audit metadata
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
ctx.audit("data_access", target="customer_records", outcome="success",
|
|
322
|
+
records_accessed=42, query="SELECT * FROM customers")
|
|
323
|
+
"""
|
|
324
|
+
if self._client:
|
|
325
|
+
self._client.log_event(
|
|
326
|
+
event_type="audit",
|
|
327
|
+
source=self.tool_name,
|
|
328
|
+
action=action,
|
|
329
|
+
target=target or self.tool_name,
|
|
330
|
+
outcome=outcome,
|
|
331
|
+
signed=True, # Top-level param for cryptographic signing
|
|
332
|
+
metadata={
|
|
333
|
+
"request_id": self.request_id,
|
|
334
|
+
**metadata
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def metadata(self) -> Dict[str, Any]:
|
|
340
|
+
"""Get request metadata."""
|
|
341
|
+
return self._metadata
|
|
342
|
+
|
|
343
|
+
def get_roots(self) -> List[str]:
|
|
344
|
+
"""
|
|
345
|
+
Get the list of filesystem roots this server can access.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List of root paths declared for this server
|
|
349
|
+
"""
|
|
350
|
+
return self._metadata.get('roots', [])
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class SecureMCP:
|
|
354
|
+
"""
|
|
355
|
+
FastMCP-compatible server with MACAW security.
|
|
356
|
+
|
|
357
|
+
All tool invocations are routed through MACAWClient for:
|
|
358
|
+
- Cryptographic signing
|
|
359
|
+
- Policy enforcement
|
|
360
|
+
- Audit logging
|
|
361
|
+
- Prompt lineage tracking
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def __init__(
|
|
365
|
+
self,
|
|
366
|
+
name: str,
|
|
367
|
+
version: str = "1.0.0",
|
|
368
|
+
intent_policy: Optional[Dict[str, Any]] = None,
|
|
369
|
+
roots: Optional[List[str]] = None,
|
|
370
|
+
**kwargs
|
|
371
|
+
):
|
|
372
|
+
"""
|
|
373
|
+
Initialize SecureMCP server.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
name: Server name (used in agent_id)
|
|
377
|
+
version: Server version
|
|
378
|
+
intent_policy: MAPL policy declaration (resources, denied_resources, etc.)
|
|
379
|
+
roots: List of filesystem paths this server can access (MCP roots)
|
|
380
|
+
**kwargs: Additional MACAWClient options (iam_token, provider, etc.)
|
|
381
|
+
|
|
382
|
+
Example:
|
|
383
|
+
# Server that can only access specific directories
|
|
384
|
+
mcp = SecureMCP(
|
|
385
|
+
"file-manager",
|
|
386
|
+
roots=["/home/user/documents", "/tmp/workspace"]
|
|
387
|
+
)
|
|
388
|
+
"""
|
|
389
|
+
self.name = name
|
|
390
|
+
self.version = version
|
|
391
|
+
self.extra_config = kwargs
|
|
392
|
+
|
|
393
|
+
# Store custom intent_policy or build default
|
|
394
|
+
self._custom_intent_policy = intent_policy
|
|
395
|
+
|
|
396
|
+
# MCP Roots - directories this server can access
|
|
397
|
+
self._roots: List[str] = roots or []
|
|
398
|
+
|
|
399
|
+
# Registries for MCP components
|
|
400
|
+
self._tools: Dict[str, Dict[str, Any]] = {}
|
|
401
|
+
self._resources: Dict[str, Dict[str, Any]] = {}
|
|
402
|
+
self._prompts: Dict[str, Dict[str, Any]] = {}
|
|
403
|
+
|
|
404
|
+
# MACAWClient (initialized in run())
|
|
405
|
+
self._client = None
|
|
406
|
+
self.agent_id = None
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def roots(self) -> List[str]:
|
|
410
|
+
"""Get the list of filesystem roots this server can access."""
|
|
411
|
+
return self._roots
|
|
412
|
+
|
|
413
|
+
def tool(
|
|
414
|
+
self,
|
|
415
|
+
name: str = None,
|
|
416
|
+
description: str = None,
|
|
417
|
+
prompts: List[str] = None
|
|
418
|
+
) -> Callable:
|
|
419
|
+
"""
|
|
420
|
+
Decorator to register a tool.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
name: Tool name (defaults to function name)
|
|
424
|
+
description: Tool description (defaults to docstring)
|
|
425
|
+
prompts: List of parameter names that should be treated as prompts
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
@mcp.tool(description="Add two numbers")
|
|
429
|
+
def add(a: float, b: float) -> float:
|
|
430
|
+
return a + b
|
|
431
|
+
|
|
432
|
+
@mcp.tool(prompts=["query"])
|
|
433
|
+
def search(query: str) -> list:
|
|
434
|
+
# query will be wrapped as AuthenticatedPrompt
|
|
435
|
+
return do_search(query)
|
|
436
|
+
"""
|
|
437
|
+
def decorator(func: Callable) -> Callable:
|
|
438
|
+
tool_name = name or func.__name__
|
|
439
|
+
tool_desc = description or func.__doc__ or ""
|
|
440
|
+
|
|
441
|
+
# Check if function wants Context parameter
|
|
442
|
+
sig = inspect.signature(func)
|
|
443
|
+
wants_context = "ctx" in sig.parameters or "context" in sig.parameters
|
|
444
|
+
|
|
445
|
+
# Store tool info
|
|
446
|
+
self._tools[tool_name] = {
|
|
447
|
+
"handler": func,
|
|
448
|
+
"description": tool_desc,
|
|
449
|
+
"wants_context": wants_context,
|
|
450
|
+
"prompts": prompts or [],
|
|
451
|
+
"parameters": self._extract_parameters(func)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
logger.debug(f"Registered tool: {tool_name}")
|
|
455
|
+
return func
|
|
456
|
+
|
|
457
|
+
return decorator
|
|
458
|
+
|
|
459
|
+
def resource(
|
|
460
|
+
self,
|
|
461
|
+
uri_pattern: str,
|
|
462
|
+
description: str = None
|
|
463
|
+
) -> Callable:
|
|
464
|
+
"""
|
|
465
|
+
Decorator to register a resource (read-only).
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
uri_pattern: URI pattern (e.g., "config://settings", "file://{path}")
|
|
469
|
+
description: Resource description
|
|
470
|
+
|
|
471
|
+
Example:
|
|
472
|
+
@mcp.resource("config://app")
|
|
473
|
+
def get_config(ctx: Context) -> dict:
|
|
474
|
+
return {"theme": "dark"}
|
|
475
|
+
"""
|
|
476
|
+
def decorator(func: Callable) -> Callable:
|
|
477
|
+
self._resources[uri_pattern] = {
|
|
478
|
+
"handler": func,
|
|
479
|
+
"description": description or func.__doc__ or "",
|
|
480
|
+
"uri_pattern": uri_pattern
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
logger.debug(f"Registered resource: {uri_pattern}")
|
|
484
|
+
return func
|
|
485
|
+
|
|
486
|
+
return decorator
|
|
487
|
+
|
|
488
|
+
def prompt(
|
|
489
|
+
self,
|
|
490
|
+
name: str = None,
|
|
491
|
+
description: str = None
|
|
492
|
+
) -> Callable:
|
|
493
|
+
"""
|
|
494
|
+
Decorator to register a prompt template.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
name: Prompt name (defaults to function name)
|
|
498
|
+
description: Prompt description
|
|
499
|
+
|
|
500
|
+
Example:
|
|
501
|
+
@mcp.prompt(description="Generate a greeting")
|
|
502
|
+
def greeting(name: str) -> str:
|
|
503
|
+
return f"Hello, {name}! How can I help?"
|
|
504
|
+
"""
|
|
505
|
+
def decorator(func: Callable) -> Callable:
|
|
506
|
+
prompt_name = name or func.__name__
|
|
507
|
+
|
|
508
|
+
self._prompts[prompt_name] = {
|
|
509
|
+
"handler": func,
|
|
510
|
+
"description": description or func.__doc__ or "",
|
|
511
|
+
"parameters": self._extract_parameters(func)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
logger.debug(f"Registered prompt: {prompt_name}")
|
|
515
|
+
return func
|
|
516
|
+
|
|
517
|
+
return decorator
|
|
518
|
+
|
|
519
|
+
def run(self, transport: str = None, port: int = 8080) -> None:
|
|
520
|
+
"""
|
|
521
|
+
Start the MCP server.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
transport: How external (non-MACAW) MCP clients may reach this server.
|
|
525
|
+
None - MACAW mesh only. Tools are reachable via invoke_tool()
|
|
526
|
+
from MACAW-aware clients. This is the default.
|
|
527
|
+
"stdio" - also serve native MCP clients over stdin/stdout.
|
|
528
|
+
NOTE: stdout becomes JSON-RPC; log to stderr instead.
|
|
529
|
+
"http" - also serve native MCP clients at http://host:port/mcp
|
|
530
|
+
port: Bind port for transport="http"
|
|
531
|
+
|
|
532
|
+
Either way every invocation goes through the MACAW PEP: policy, signing,
|
|
533
|
+
and audit are identical on both paths.
|
|
534
|
+
"""
|
|
535
|
+
asyncio.run(self._run_async(transport, port))
|
|
536
|
+
|
|
537
|
+
async def _run_async(self, transport: str = None, port: int = 8080) -> None:
|
|
538
|
+
"""Async server run."""
|
|
539
|
+
try:
|
|
540
|
+
# Initialize MACAWClient with registered tools
|
|
541
|
+
self._init_macaw_client()
|
|
542
|
+
|
|
543
|
+
logger.info(f"SecureMCP server '{self.name}' started")
|
|
544
|
+
logger.info(f" Agent ID: {self.agent_id}")
|
|
545
|
+
logger.info(f" Tools: {list(self._tools.keys())}")
|
|
546
|
+
logger.info(f" Resources: {list(self._resources.keys())}")
|
|
547
|
+
logger.info(f" Prompts: {list(self._prompts.keys())}")
|
|
548
|
+
|
|
549
|
+
if transport is None:
|
|
550
|
+
# MACAW mesh only. The listener lives inside MACAWClient (started by
|
|
551
|
+
# register()); this loop just keeps the process alive.
|
|
552
|
+
while True:
|
|
553
|
+
await asyncio.sleep(1)
|
|
554
|
+
else:
|
|
555
|
+
from ._serve import serve
|
|
556
|
+
await serve(
|
|
557
|
+
self.name,
|
|
558
|
+
self.version,
|
|
559
|
+
registry=self._client,
|
|
560
|
+
target_agent=self.agent_id,
|
|
561
|
+
prefix="",
|
|
562
|
+
transport=transport,
|
|
563
|
+
port=port,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
except KeyboardInterrupt:
|
|
567
|
+
logger.info("Shutting down...")
|
|
568
|
+
finally:
|
|
569
|
+
if self._client:
|
|
570
|
+
self._client.unregister()
|
|
571
|
+
logger.info(f"Unregistered agent: {self.agent_id}")
|
|
572
|
+
|
|
573
|
+
def _init_macaw_client(self) -> None:
|
|
574
|
+
"""Initialize MACAWClient with all registered tools."""
|
|
575
|
+
try:
|
|
576
|
+
from macaw_client import MACAWClient
|
|
577
|
+
|
|
578
|
+
# Collect unified tools config (handler + metadata together)
|
|
579
|
+
tools_config = {}
|
|
580
|
+
tool_names = []
|
|
581
|
+
|
|
582
|
+
for tool_name, tool_info in self._tools.items():
|
|
583
|
+
tool_names.append(tool_name)
|
|
584
|
+
# Unified format: handler + metadata together
|
|
585
|
+
tools_config[tool_name] = {
|
|
586
|
+
"handler": self._create_handler(
|
|
587
|
+
tool_name,
|
|
588
|
+
tool_info["handler"],
|
|
589
|
+
tool_info["wants_context"]
|
|
590
|
+
),
|
|
591
|
+
"description": tool_info["description"],
|
|
592
|
+
"prompts": tool_info.get("prompts", []),
|
|
593
|
+
# Schema lives on the MACAW registry so the MCP endpoint can
|
|
594
|
+
# answer tools/list from the same place policy is enforced.
|
|
595
|
+
"metadata": {"schema": _json_schema(tool_info["parameters"])}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
# Register resources as tools with resource: prefix
|
|
599
|
+
for uri_pattern, res_info in self._resources.items():
|
|
600
|
+
res_tool_name = f"resource:{uri_pattern}"
|
|
601
|
+
tool_names.append(res_tool_name)
|
|
602
|
+
tools_config[res_tool_name] = {
|
|
603
|
+
"handler": self._create_handler(
|
|
604
|
+
uri_pattern,
|
|
605
|
+
res_info["handler"],
|
|
606
|
+
True # Resources always get context
|
|
607
|
+
),
|
|
608
|
+
"description": res_info["description"],
|
|
609
|
+
"read_only": True
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
# Register prompts as tools with prompt: prefix
|
|
613
|
+
for prompt_name, prompt_info in self._prompts.items():
|
|
614
|
+
prompt_tool_name = f"prompt:{prompt_name}"
|
|
615
|
+
tool_names.append(prompt_tool_name)
|
|
616
|
+
tools_config[prompt_tool_name] = {
|
|
617
|
+
"handler": self._create_handler(
|
|
618
|
+
prompt_name,
|
|
619
|
+
prompt_info["handler"],
|
|
620
|
+
False
|
|
621
|
+
),
|
|
622
|
+
"description": prompt_info["description"]
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# Build intent policy
|
|
626
|
+
if self._custom_intent_policy:
|
|
627
|
+
intent_policy = self._custom_intent_policy
|
|
628
|
+
else:
|
|
629
|
+
intent_policy = {
|
|
630
|
+
"provided_capabilities": [{
|
|
631
|
+
"type": "tools",
|
|
632
|
+
"tools": tool_names,
|
|
633
|
+
"description": f"Tools provided by SecureMCP server {self.name}"
|
|
634
|
+
}],
|
|
635
|
+
"required_resources": [],
|
|
636
|
+
"data_access": ["none"],
|
|
637
|
+
"description": f"SecureMCP server: {self.name}"
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# Add roots as MAPL resource declarations
|
|
641
|
+
if self._roots:
|
|
642
|
+
intent_policy["roots"] = self._roots
|
|
643
|
+
intent_policy["required_resources"] = [
|
|
644
|
+
{
|
|
645
|
+
"resource": f"file://{root}/*",
|
|
646
|
+
"operations": ["read", "write", "list"],
|
|
647
|
+
"justification": f"MCP root: {root}"
|
|
648
|
+
}
|
|
649
|
+
for root in self._roots
|
|
650
|
+
]
|
|
651
|
+
intent_policy["data_access"] = ["filesystem"]
|
|
652
|
+
|
|
653
|
+
# Create MACAWClient with unified tools config
|
|
654
|
+
self._client = MACAWClient(
|
|
655
|
+
app_name=f"securemcp-{self.name}",
|
|
656
|
+
app_version=self.version,
|
|
657
|
+
intent_policy=intent_policy,
|
|
658
|
+
tools=tools_config, # Unified: {name: {handler, description, ...}}
|
|
659
|
+
**self.extra_config
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
# Register with Local Agent
|
|
663
|
+
if not self._client.register():
|
|
664
|
+
raise RuntimeError("Failed to register with MACAW Local Agent")
|
|
665
|
+
|
|
666
|
+
self.agent_id = self._client.agent_id
|
|
667
|
+
logger.info(f"Registered as agent: {self.agent_id}")
|
|
668
|
+
|
|
669
|
+
except ImportError as e:
|
|
670
|
+
raise RuntimeError(f"MACAWClient not available: {e}")
|
|
671
|
+
except Exception as e:
|
|
672
|
+
raise RuntimeError(f"MACAW initialization failed: {e}")
|
|
673
|
+
|
|
674
|
+
def _create_handler(
|
|
675
|
+
self,
|
|
676
|
+
tool_name: str,
|
|
677
|
+
func: Callable,
|
|
678
|
+
wants_context: bool
|
|
679
|
+
) -> Callable:
|
|
680
|
+
"""Create a handler wrapper for MACAWClient."""
|
|
681
|
+
def handler(params: Dict[str, Any]) -> Any:
|
|
682
|
+
try:
|
|
683
|
+
if wants_context:
|
|
684
|
+
# Get source_agent from MACAWClient's current request context
|
|
685
|
+
# This is set during tool execution handling
|
|
686
|
+
source_agent = ""
|
|
687
|
+
if self._client and hasattr(self._client, 'current_request_context'):
|
|
688
|
+
source_agent = self._client.current_request_context.get('source_agent', '')
|
|
689
|
+
logger.debug(f"[SecureMCP] Tool '{tool_name}' invoked by source_agent: {source_agent}")
|
|
690
|
+
|
|
691
|
+
# Create context for this invocation
|
|
692
|
+
# Include roots in metadata for get_roots() access
|
|
693
|
+
metadata = params.pop("_metadata", {})
|
|
694
|
+
metadata["roots"] = self._roots
|
|
695
|
+
ctx = Context(
|
|
696
|
+
request_id=params.pop("_request_id", ""),
|
|
697
|
+
tool_name=tool_name,
|
|
698
|
+
source_agent=source_agent, # For sampling/elicitation callbacks
|
|
699
|
+
_client=self._client,
|
|
700
|
+
_metadata=metadata
|
|
701
|
+
)
|
|
702
|
+
# Check parameter name (ctx or context)
|
|
703
|
+
sig = inspect.signature(func)
|
|
704
|
+
if "ctx" in sig.parameters:
|
|
705
|
+
params["ctx"] = ctx
|
|
706
|
+
elif "context" in sig.parameters:
|
|
707
|
+
params["context"] = ctx
|
|
708
|
+
|
|
709
|
+
# Execute handler
|
|
710
|
+
result = func(**params)
|
|
711
|
+
|
|
712
|
+
# Handle async functions
|
|
713
|
+
if asyncio.iscoroutine(result):
|
|
714
|
+
# Use asyncio.run() which creates a new event loop
|
|
715
|
+
# This works in threads without an existing event loop
|
|
716
|
+
result = asyncio.run(result)
|
|
717
|
+
|
|
718
|
+
# Wrap result in dict format expected by MACAWClient
|
|
719
|
+
if not isinstance(result, dict):
|
|
720
|
+
result = {"result": result}
|
|
721
|
+
|
|
722
|
+
return result
|
|
723
|
+
|
|
724
|
+
except Exception as e:
|
|
725
|
+
logger.error(f"Handler error for {tool_name}: {e}")
|
|
726
|
+
raise
|
|
727
|
+
|
|
728
|
+
return handler
|
|
729
|
+
|
|
730
|
+
def _extract_parameters(self, func: Callable) -> Dict[str, Any]:
|
|
731
|
+
"""Extract parameter info from function signature."""
|
|
732
|
+
sig = inspect.signature(func)
|
|
733
|
+
params = {}
|
|
734
|
+
|
|
735
|
+
for param_name, param in sig.parameters.items():
|
|
736
|
+
if param_name in ("self", "ctx", "context"):
|
|
737
|
+
continue
|
|
738
|
+
|
|
739
|
+
param_info = {
|
|
740
|
+
"required": param.default == inspect.Parameter.empty
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
# Map type annotations to JSON schema types
|
|
744
|
+
if param.annotation != inspect.Parameter.empty:
|
|
745
|
+
type_map = {
|
|
746
|
+
str: "string",
|
|
747
|
+
int: "integer",
|
|
748
|
+
float: "number",
|
|
749
|
+
bool: "boolean",
|
|
750
|
+
dict: "object",
|
|
751
|
+
list: "array"
|
|
752
|
+
}
|
|
753
|
+
param_info["type"] = type_map.get(param.annotation, "string")
|
|
754
|
+
else:
|
|
755
|
+
param_info["type"] = "string"
|
|
756
|
+
|
|
757
|
+
if param.default != inspect.Parameter.empty:
|
|
758
|
+
param_info["default"] = param.default
|
|
759
|
+
|
|
760
|
+
params[param_name] = param_info
|
|
761
|
+
|
|
762
|
+
return params
|
|
763
|
+
|
|
764
|
+
# Convenience methods for testing
|
|
765
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
|
766
|
+
"""
|
|
767
|
+
Call a tool programmatically (for testing).
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
tool_name: Name of tool to call
|
|
771
|
+
arguments: Tool arguments
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
Tool result
|
|
775
|
+
"""
|
|
776
|
+
if tool_name not in self._tools:
|
|
777
|
+
raise ValueError(f"Unknown tool: {tool_name}")
|
|
778
|
+
|
|
779
|
+
handler = self._tools[tool_name]["handler"]
|
|
780
|
+
result = handler(**arguments)
|
|
781
|
+
|
|
782
|
+
if asyncio.iscoroutine(result):
|
|
783
|
+
result = await result
|
|
784
|
+
|
|
785
|
+
return result
|