@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.
Files changed (142) hide show
  1. package/LICENSE.md +339 -0
  2. package/bin/install.js +422 -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,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