@macaw-ai/client 0.0.0-stage → 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +339 -0
- package/bin/install.js +278 -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,248 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Example: SecureMCP Calculator Server
|
|
4
|
+
|
|
5
|
+
Demonstrates the FastMCP-compatible SecureMCP API:
|
|
6
|
+
- @mcp.tool decorator for registering tools
|
|
7
|
+
- @mcp.resource decorator for read-only resources
|
|
8
|
+
- @mcp.prompt decorator for prompt templates
|
|
9
|
+
- Context object for vault access
|
|
10
|
+
- Context logging: ctx.debug(), ctx.info(), ctx.warning(), ctx.error()
|
|
11
|
+
- Context audit: ctx.audit() for signed audit entries
|
|
12
|
+
- Progress reporting: ctx.report_progress()
|
|
13
|
+
- mcp.run() for starting the server
|
|
14
|
+
|
|
15
|
+
This is the recommended way to build MCP servers with MACAW security.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from typing import List, Dict, Any
|
|
20
|
+
|
|
21
|
+
from macaw_adapters.mcp import SecureMCP, Context
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Create SecureMCP server - same pattern as FastMCP
|
|
25
|
+
mcp = SecureMCP("calculator", version="1.0.0")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Tool: Add two numbers
|
|
29
|
+
@mcp.tool(description="Add two numbers together")
|
|
30
|
+
def add(a: float, b: float) -> float:
|
|
31
|
+
"""Add a + b and return the result."""
|
|
32
|
+
return a + b
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Tool: Subtract two numbers
|
|
36
|
+
@mcp.tool(description="Subtract b from a")
|
|
37
|
+
def subtract(a: float, b: float) -> float:
|
|
38
|
+
"""Subtract a - b and return the result."""
|
|
39
|
+
return a - b
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Tool: Multiply two numbers
|
|
43
|
+
@mcp.tool(description="Multiply two numbers")
|
|
44
|
+
def multiply(a: float, b: float) -> float:
|
|
45
|
+
"""Multiply a * b and return the result."""
|
|
46
|
+
return a * b
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Tool: Divide two numbers
|
|
50
|
+
@mcp.tool(description="Divide a by b")
|
|
51
|
+
def divide(a: float, b: float) -> dict:
|
|
52
|
+
"""Divide a / b and return the result."""
|
|
53
|
+
if b == 0:
|
|
54
|
+
return {"error": "Division by zero"}
|
|
55
|
+
return {"result": a / b}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Tool with Context: Calculator with history tracking and logging
|
|
59
|
+
@mcp.tool(description="Calculate and track history")
|
|
60
|
+
def calculate(ctx: Context, operation: str, a: float, b: float) -> Dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Perform calculation and store in history.
|
|
63
|
+
|
|
64
|
+
Demonstrates Context usage for:
|
|
65
|
+
- Storing results in context vault (ctx.set)
|
|
66
|
+
- Retrieving previous values (ctx.get)
|
|
67
|
+
- Logging with ctx.info(), ctx.debug(), ctx.warning()
|
|
68
|
+
- Audit logging with ctx.audit()
|
|
69
|
+
"""
|
|
70
|
+
ctx.info(f"Calculating: {a} {operation} {b}")
|
|
71
|
+
|
|
72
|
+
# Map operations to functions
|
|
73
|
+
ops = {
|
|
74
|
+
"add": lambda x, y: x + y,
|
|
75
|
+
"subtract": lambda x, y: x - y,
|
|
76
|
+
"multiply": lambda x, y: x * y,
|
|
77
|
+
"divide": lambda x, y: x / y if y != 0 else "error:div_by_zero"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if operation not in ops:
|
|
81
|
+
ctx.warning(f"Unknown operation requested: {operation}")
|
|
82
|
+
return {
|
|
83
|
+
"result": None,
|
|
84
|
+
"status": "invalid_operation",
|
|
85
|
+
"message": f"Unknown operation: {operation}"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Check for division by zero
|
|
89
|
+
if operation == "divide" and b == 0:
|
|
90
|
+
ctx.warning("Division by zero attempted")
|
|
91
|
+
|
|
92
|
+
result = ops[operation](a, b)
|
|
93
|
+
ctx.debug(f"Result computed: {result}")
|
|
94
|
+
|
|
95
|
+
# Get existing history from context vault
|
|
96
|
+
history = ctx.get("calc_history") or []
|
|
97
|
+
|
|
98
|
+
# Add this calculation to history
|
|
99
|
+
entry = {
|
|
100
|
+
"operation": operation,
|
|
101
|
+
"a": a,
|
|
102
|
+
"b": b,
|
|
103
|
+
"result": result
|
|
104
|
+
}
|
|
105
|
+
history.append(entry)
|
|
106
|
+
|
|
107
|
+
# Store updated history in context vault
|
|
108
|
+
ctx.set("calc_history", history)
|
|
109
|
+
|
|
110
|
+
# Audit the calculation
|
|
111
|
+
ctx.audit(
|
|
112
|
+
action="calculation",
|
|
113
|
+
target="calculator",
|
|
114
|
+
outcome="success",
|
|
115
|
+
operation=operation,
|
|
116
|
+
operands=[a, b],
|
|
117
|
+
result=result
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"result": result,
|
|
122
|
+
"expression": f"{a} {operation} {b} = {result}",
|
|
123
|
+
"history_count": len(history)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Tool with Progress: Batch calculations
|
|
128
|
+
@mcp.tool(description="Perform multiple calculations with progress")
|
|
129
|
+
async def batch_calculate(ctx: Context, calculations: list) -> Dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Perform multiple calculations, reporting progress.
|
|
132
|
+
|
|
133
|
+
Demonstrates:
|
|
134
|
+
- ctx.report_progress() for long-running operations
|
|
135
|
+
- Async tool execution
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
calculations: List of {"op": str, "a": float, "b": float}
|
|
139
|
+
"""
|
|
140
|
+
total = len(calculations)
|
|
141
|
+
if total == 0:
|
|
142
|
+
return {"results": [], "count": 0}
|
|
143
|
+
|
|
144
|
+
ctx.info(f"Starting batch of {total} calculations")
|
|
145
|
+
await ctx.report_progress(0.0, f"Processing {total} calculations")
|
|
146
|
+
|
|
147
|
+
ops = {
|
|
148
|
+
"add": lambda x, y: x + y,
|
|
149
|
+
"subtract": lambda x, y: x - y,
|
|
150
|
+
"multiply": lambda x, y: x * y,
|
|
151
|
+
"divide": lambda x, y: x / y if y != 0 else "error:div_by_zero"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
results = []
|
|
155
|
+
for i, calc in enumerate(calculations):
|
|
156
|
+
op = calc.get("op", "add")
|
|
157
|
+
a = calc.get("a", 0)
|
|
158
|
+
b = calc.get("b", 0)
|
|
159
|
+
|
|
160
|
+
progress = i / total
|
|
161
|
+
await ctx.report_progress(progress, f"Calculating {i+1}/{total}: {a} {op} {b}")
|
|
162
|
+
|
|
163
|
+
if op in ops:
|
|
164
|
+
result = ops[op](a, b)
|
|
165
|
+
results.append({"expression": f"{a} {op} {b}", "result": result})
|
|
166
|
+
else:
|
|
167
|
+
results.append({"expression": f"{a} {op} {b}", "error": "unknown op"})
|
|
168
|
+
|
|
169
|
+
await asyncio.sleep(0.1) # Simulate work
|
|
170
|
+
|
|
171
|
+
await ctx.report_progress(1.0, "Batch complete")
|
|
172
|
+
ctx.info(f"Batch complete: {total} calculations")
|
|
173
|
+
|
|
174
|
+
ctx.audit(
|
|
175
|
+
action="batch_calculation",
|
|
176
|
+
target="calculator",
|
|
177
|
+
outcome="success",
|
|
178
|
+
count=total
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return {"results": results, "count": total}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Resource: Get calculation history
|
|
185
|
+
@mcp.resource("calc://history", description="Get calculation history")
|
|
186
|
+
def get_history(ctx: Context) -> List[Dict[str, Any]]:
|
|
187
|
+
"""
|
|
188
|
+
Read-only resource that returns calculation history.
|
|
189
|
+
|
|
190
|
+
Resources are treated as read-only tools in SecureMCP.
|
|
191
|
+
"""
|
|
192
|
+
history = ctx.get("calc_history") or []
|
|
193
|
+
return {
|
|
194
|
+
"history": history,
|
|
195
|
+
"count": len(history)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Prompt: Generate a calculation prompt
|
|
200
|
+
@mcp.prompt(description="Generate a prompt for calculations")
|
|
201
|
+
def calculation_prompt(numbers: str, operation: str = "add") -> str:
|
|
202
|
+
"""
|
|
203
|
+
Generate a calculation prompt template.
|
|
204
|
+
|
|
205
|
+
Prompts are starting points for AuthenticatedPrompt creation.
|
|
206
|
+
"""
|
|
207
|
+
return f"Please {operation} the following numbers: {numbers}"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
import sys
|
|
212
|
+
|
|
213
|
+
# Optional transport for native (non-MACAW) MCP clients:
|
|
214
|
+
# python securemcp_calculator.py MACAW mesh only
|
|
215
|
+
# python securemcp_calculator.py stdio also serve Claude Desktop / Cursor
|
|
216
|
+
# python securemcp_calculator.py http also serve http://localhost:8080/mcp
|
|
217
|
+
transport = sys.argv[1] if len(sys.argv) > 1 else None
|
|
218
|
+
|
|
219
|
+
# stdout is the JSON-RPC stream when transport="stdio", so the banner goes to stderr.
|
|
220
|
+
out = sys.stderr
|
|
221
|
+
|
|
222
|
+
print("=" * 60, file=out)
|
|
223
|
+
print("SecureMCP Calculator Server", file=out)
|
|
224
|
+
print("=" * 60, file=out)
|
|
225
|
+
print(file=out)
|
|
226
|
+
print("This server provides:", file=out)
|
|
227
|
+
print(" Tools: add, subtract, multiply, divide, calculate, batch_calculate", file=out)
|
|
228
|
+
print(" Resources: calc://history", file=out)
|
|
229
|
+
print(" Prompts: calculation_prompt", file=out)
|
|
230
|
+
print(file=out)
|
|
231
|
+
print("Demonstrates:", file=out)
|
|
232
|
+
print(" - ctx.info/debug/warning/error() - Logging", file=out)
|
|
233
|
+
print(" - ctx.audit() - Signed audit entries", file=out)
|
|
234
|
+
print(" - ctx.report_progress() - Progress reporting", file=out)
|
|
235
|
+
print(" - ctx.get/set() - Context vault", file=out)
|
|
236
|
+
print(file=out)
|
|
237
|
+
print("All invocations are secured by MACAW:", file=out)
|
|
238
|
+
print(" - Cryptographic signing", file=out)
|
|
239
|
+
print(" - Policy enforcement", file=out)
|
|
240
|
+
print(" - Audit logging", file=out)
|
|
241
|
+
print(file=out)
|
|
242
|
+
if transport:
|
|
243
|
+
print(f"Also serving native MCP clients over: {transport}", file=out)
|
|
244
|
+
print("Starting server...", file=out)
|
|
245
|
+
print("=" * 60, file=out)
|
|
246
|
+
|
|
247
|
+
# Run the server
|
|
248
|
+
mcp.run(transport=transport)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SecureMCP Calculator Server
|
|
4
|
+
|
|
5
|
+
Run this first, then use 1a_simple_invocation.py to test.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from macaw_adapters.mcp import SecureMCP, Context
|
|
9
|
+
|
|
10
|
+
mcp = SecureMCP("calculator")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool(description="Add two numbers")
|
|
14
|
+
def add(a: float, b: float) -> float:
|
|
15
|
+
return a + b
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@mcp.tool(description="Subtract two numbers")
|
|
19
|
+
def subtract(a: float, b: float) -> float:
|
|
20
|
+
return a - b
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp.tool(description="Multiply two numbers")
|
|
24
|
+
def multiply(a: float, b: float) -> float:
|
|
25
|
+
return a * b
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@mcp.tool(description="Divide two numbers")
|
|
29
|
+
def divide(a: float, b: float) -> float:
|
|
30
|
+
if b == 0:
|
|
31
|
+
return float('inf')
|
|
32
|
+
return a / b
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@mcp.tool(description="Calculate with history tracking")
|
|
36
|
+
def calculate(ctx: Context, op: str, a: float, b: float) -> dict:
|
|
37
|
+
"""Perform calculation and track in history."""
|
|
38
|
+
ops = {
|
|
39
|
+
"add": lambda x, y: x + y,
|
|
40
|
+
"subtract": lambda x, y: x - y,
|
|
41
|
+
"multiply": lambda x, y: x * y,
|
|
42
|
+
"divide": lambda x, y: x / y if y != 0 else float('inf')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if op not in ops:
|
|
46
|
+
return {"error": f"Unknown operation: {op}"}
|
|
47
|
+
|
|
48
|
+
result = ops[op](a, b)
|
|
49
|
+
|
|
50
|
+
# Track in context vault
|
|
51
|
+
history = ctx.get("calc_history") or []
|
|
52
|
+
history.append({"op": op, "a": a, "b": b, "result": result})
|
|
53
|
+
ctx.set("calc_history", history)
|
|
54
|
+
|
|
55
|
+
return {"result": result, "history_count": len(history)}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp.resource("calc://history")
|
|
59
|
+
def get_history(ctx: Context) -> dict:
|
|
60
|
+
"""Get calculation history from context vault."""
|
|
61
|
+
history = ctx.get("calc_history") or []
|
|
62
|
+
return {"history": history, "count": len(history)}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
print("=" * 50)
|
|
67
|
+
print("SecureMCP Calculator Server")
|
|
68
|
+
print("=" * 50)
|
|
69
|
+
print()
|
|
70
|
+
print("Tools: add, subtract, multiply, divide, calculate")
|
|
71
|
+
print("Resources: calc://history")
|
|
72
|
+
print()
|
|
73
|
+
print("Press Ctrl+C to stop")
|
|
74
|
+
print("=" * 50)
|
|
75
|
+
mcp.run()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
openai_1a_dropin_simple.py - Drop-in replacement for OpenAI client
|
|
4
|
+
|
|
5
|
+
The simplest integration path - just replace your import and it works.
|
|
6
|
+
No Identity Provider needed.
|
|
7
|
+
|
|
8
|
+
Prerequisites:
|
|
9
|
+
- MACAW SDK installed (pip install macaw-client macaw-adapters)
|
|
10
|
+
- OPENAI_API_KEY environment variable
|
|
11
|
+
|
|
12
|
+
Run:
|
|
13
|
+
export OPENAI_API_KEY=sk-...
|
|
14
|
+
python openai_1a_dropin_simple.py
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# BEFORE: from openai import OpenAI
|
|
21
|
+
# AFTER:
|
|
22
|
+
from macaw_adapters.openai import SecureOpenAI
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
# Check for API key
|
|
26
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
27
|
+
print("Set OPENAI_API_KEY environment variable")
|
|
28
|
+
print(" export OPENAI_API_KEY=sk-...")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
print("=" * 60)
|
|
32
|
+
print("Example 1a: Drop-in Replacement (OpenAI)")
|
|
33
|
+
print("=" * 60)
|
|
34
|
+
print("\nPath: Direct on service (simplest)")
|
|
35
|
+
print("Use when: Single app, no user distinction, app-level policies")
|
|
36
|
+
|
|
37
|
+
# Create SecureOpenAI - just like regular OpenAI client
|
|
38
|
+
# Add intent_policy to restrict to GPT-3.5 only (no GPT-4)
|
|
39
|
+
client = SecureOpenAI(
|
|
40
|
+
app_name="my-simple-app",
|
|
41
|
+
intent_policy={
|
|
42
|
+
"constraints": {
|
|
43
|
+
"parameters": {
|
|
44
|
+
"tool:*/generate": {
|
|
45
|
+
"model": ["gpt-3.5-turbo"], # Only GPT-3.5 allowed
|
|
46
|
+
"max_tokens": {"max": 200}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
print(f"\nService registered: {client.server_id}")
|
|
54
|
+
print(f"Mode: {client._mode}")
|
|
55
|
+
print("Policy: GPT-3.5 only, max 200 tokens")
|
|
56
|
+
|
|
57
|
+
# Same API as OpenAI - no changes needed
|
|
58
|
+
print("\n--- Test 1: Allowed model (gpt-3.5-turbo) ---")
|
|
59
|
+
response = client.chat.completions.create(
|
|
60
|
+
model="gpt-3.5-turbo",
|
|
61
|
+
max_tokens=100,
|
|
62
|
+
messages=[
|
|
63
|
+
{"role": "user", "content": "What is compound interest? Answer briefly."}
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
print(f"Model: {response.model}")
|
|
68
|
+
print(f"Response: {response.choices[0].message.content}")
|
|
69
|
+
|
|
70
|
+
# Policy enforcement - try a blocked model
|
|
71
|
+
print("\n--- Test 2: Blocked model (gpt-4) ---")
|
|
72
|
+
try:
|
|
73
|
+
response = client.chat.completions.create(
|
|
74
|
+
model="gpt-4", # NOT in allowed list!
|
|
75
|
+
max_tokens=100,
|
|
76
|
+
messages=[
|
|
77
|
+
{"role": "user", "content": "What is compound interest?"}
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
print(f"Unexpected success - should have been blocked!")
|
|
81
|
+
print(f"Response: {response.choices[0].message.content[:100]}...")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f"Correctly blocked: {e}")
|
|
84
|
+
|
|
85
|
+
print("\n" + "=" * 60)
|
|
86
|
+
print("Example complete!")
|
|
87
|
+
print("=" * 60)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
try:
|
|
92
|
+
main()
|
|
93
|
+
except Exception as e:
|
|
94
|
+
err = str(e)
|
|
95
|
+
print("\n" + "=" * 60)
|
|
96
|
+
if "OPENAI_API_KEY" in err or "api_key" in err.lower():
|
|
97
|
+
print("ERROR: OpenAI API key not configured")
|
|
98
|
+
print("Fix: export OPENAI_API_KEY=sk-...")
|
|
99
|
+
else:
|
|
100
|
+
print(f"ERROR: {e}")
|
|
101
|
+
print("=" * 60)
|
|
102
|
+
sys.exit(1)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
openai_1b_multiuser_bind.py - Multi-user OpenAI with bind_to_user
|
|
4
|
+
|
|
5
|
+
Each user's JWT identity flows through for policy evaluation.
|
|
6
|
+
Different users get different permissions based on their policies.
|
|
7
|
+
|
|
8
|
+
Prerequisites:
|
|
9
|
+
- MACAW SDK installed (pip install macaw-client macaw-adapters)
|
|
10
|
+
- OPENAI_API_KEY environment variable
|
|
11
|
+
- Identity Provider configured (Console -> Settings -> Identity Bridge)
|
|
12
|
+
- Test users: alice/Alice123!, bob/Bob@123!
|
|
13
|
+
|
|
14
|
+
Run:
|
|
15
|
+
export OPENAI_API_KEY=sk-...
|
|
16
|
+
python openai_1b_multiuser_bind.py
|
|
17
|
+
|
|
18
|
+
No IdP configured? Run simpler example first:
|
|
19
|
+
python openai_1a_dropin_simple.py
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from macaw_adapters.openai import SecureOpenAI
|
|
26
|
+
from macaw_client import MACAWClient, RemoteIdentityProvider
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Test configurations based on user policies
|
|
30
|
+
# Alice: GPT-3.5 only, max 500 tokens
|
|
31
|
+
# Bob: GPT-3.5/4, max 2000 tokens
|
|
32
|
+
USER_TESTS = {
|
|
33
|
+
"alice": {
|
|
34
|
+
"password": "Alice123!",
|
|
35
|
+
"policy_desc": "GPT-3.5 only, max 500 tokens",
|
|
36
|
+
"tests": [
|
|
37
|
+
# (model, max_tokens, should_succeed)
|
|
38
|
+
("gpt-3.5-turbo", 400, True), # ALLOWED - correct model, within limit
|
|
39
|
+
("gpt-4", 400, False), # BLOCKED - alice can't use GPT-4
|
|
40
|
+
("gpt-3.5-turbo", 600, False), # BLOCKED - exceeds max_tokens
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"bob": {
|
|
44
|
+
"password": "Bob@123!",
|
|
45
|
+
"policy_desc": "GPT-3.5/4, max 2000 tokens",
|
|
46
|
+
"tests": [
|
|
47
|
+
("gpt-3.5-turbo", 400, True), # ALLOWED
|
|
48
|
+
("gpt-4", 400, True), # ALLOWED - bob CAN use GPT-4
|
|
49
|
+
("gpt-3.5-turbo", 600, True), # ALLOWED - within bob's 2000 limit
|
|
50
|
+
("gpt-4", 2500, False), # BLOCKED - exceeds max_tokens
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_user_client(username: str, password: str) -> MACAWClient:
|
|
57
|
+
"""Create authenticated user client."""
|
|
58
|
+
print(f" Authenticating {username}...")
|
|
59
|
+
jwt_token, _ = RemoteIdentityProvider().login(username, password)
|
|
60
|
+
|
|
61
|
+
user = MACAWClient(
|
|
62
|
+
user_name=username,
|
|
63
|
+
iam_token=jwt_token,
|
|
64
|
+
agent_type="user",
|
|
65
|
+
app_name="financial-app"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if not user.register():
|
|
69
|
+
raise RuntimeError(f"Failed to register user {username}")
|
|
70
|
+
|
|
71
|
+
print(f" User agent: {user.agent_id}")
|
|
72
|
+
return user
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_user(username: str, openai_service: SecureOpenAI):
|
|
76
|
+
"""Test a user with bind_to_user pattern."""
|
|
77
|
+
config = USER_TESTS[username]
|
|
78
|
+
|
|
79
|
+
print(f"\n{'=' * 60}")
|
|
80
|
+
print(f"Testing {username.upper()} via bind_to_user")
|
|
81
|
+
print(f"Policy: {config['policy_desc']}")
|
|
82
|
+
print("=" * 60)
|
|
83
|
+
|
|
84
|
+
# 1. Create user client with JWT
|
|
85
|
+
user_client = create_user_client(username, config["password"])
|
|
86
|
+
|
|
87
|
+
# 2. Bind user to service
|
|
88
|
+
print(f"\n Binding to service: {openai_service.server_id}")
|
|
89
|
+
user_openai = openai_service.bind_to_user(user_client)
|
|
90
|
+
|
|
91
|
+
# 3. Test with different parameters
|
|
92
|
+
print(f"\n Running tests:")
|
|
93
|
+
for model, max_tokens, should_succeed in config["tests"]:
|
|
94
|
+
expected = "SHOULD SUCCEED" if should_succeed else "SHOULD BE BLOCKED"
|
|
95
|
+
print(f"\n -> {model}, {max_tokens} tokens ({expected})")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
response = user_openai.chat.completions.create(
|
|
99
|
+
model=model,
|
|
100
|
+
max_tokens=max_tokens,
|
|
101
|
+
messages=[
|
|
102
|
+
{"role": "system", "content": "You are a financial analyst."},
|
|
103
|
+
{"role": "user", "content": "What is revenue growth? Brief answer."}
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if should_succeed:
|
|
108
|
+
print(f" SUCCESS (as expected)")
|
|
109
|
+
# Non-streaming: response is a ChatCompletion object
|
|
110
|
+
content = response.choices[0].message.content
|
|
111
|
+
print(f" Model: {response.model}")
|
|
112
|
+
print(f" Response: {content[:60]}...")
|
|
113
|
+
else:
|
|
114
|
+
print(f" UNEXPECTED SUCCESS - should have been blocked!")
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
error = str(e)
|
|
118
|
+
is_policy_block = any(word in error.lower() for word in
|
|
119
|
+
["not in allowed", "not permitted", "policy",
|
|
120
|
+
"max_tokens", "model", "blocked", "denied"])
|
|
121
|
+
if is_policy_block:
|
|
122
|
+
if should_succeed:
|
|
123
|
+
print(f" UNEXPECTED BLOCK: {error[:60]}...")
|
|
124
|
+
else:
|
|
125
|
+
print(f" CORRECTLY BLOCKED by policy")
|
|
126
|
+
else:
|
|
127
|
+
print(f" ERROR: {error[:60]}...")
|
|
128
|
+
|
|
129
|
+
# 4. Cleanup
|
|
130
|
+
print(f"\n Unbinding user...")
|
|
131
|
+
user_openai.unbind()
|
|
132
|
+
print(f" is_bound: {user_openai.is_bound}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
# Check for API key
|
|
137
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
138
|
+
print("Set OPENAI_API_KEY environment variable")
|
|
139
|
+
print(" export OPENAI_API_KEY=sk-...")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
print("=" * 60)
|
|
143
|
+
print("Example 1b: Multi-user with bind_to_user (OpenAI)")
|
|
144
|
+
print("=" * 60)
|
|
145
|
+
print("\nPath: bind_to_user (per-user identity)")
|
|
146
|
+
print("Use when: SaaS app, different users need different permissions")
|
|
147
|
+
print("\nArchitecture:")
|
|
148
|
+
print(" - Single SecureOpenAI service (shared)")
|
|
149
|
+
print(" - Per-user MACAWClient with JWT identity")
|
|
150
|
+
print(" - bind_to_user() connects user to service")
|
|
151
|
+
print(" - User's identity flows through for policy evaluation")
|
|
152
|
+
|
|
153
|
+
# Create SINGLE service (shared across all users)
|
|
154
|
+
print("\n--- Creating SecureOpenAI service ---")
|
|
155
|
+
openai_service = SecureOpenAI(app_name="openai-service")
|
|
156
|
+
print(f"Service registered: {openai_service.server_id}")
|
|
157
|
+
print(f"Mode: {openai_service._mode}")
|
|
158
|
+
|
|
159
|
+
# Test alice (restricted)
|
|
160
|
+
try:
|
|
161
|
+
test_user("alice", openai_service)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"\nFailed to test alice: {e}")
|
|
164
|
+
|
|
165
|
+
# Test bob (enhanced)
|
|
166
|
+
try:
|
|
167
|
+
test_user("bob", openai_service)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
print(f"\nFailed to test bob: {e}")
|
|
170
|
+
|
|
171
|
+
print("\n" + "=" * 60)
|
|
172
|
+
print("Example complete!")
|
|
173
|
+
print("=" * 60)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
try:
|
|
178
|
+
main()
|
|
179
|
+
except Exception as e:
|
|
180
|
+
err = str(e)
|
|
181
|
+
print("\n" + "=" * 60)
|
|
182
|
+
if "OPENAI_API_KEY" in err or "api_key" in err.lower():
|
|
183
|
+
print("ERROR: OpenAI API key not configured")
|
|
184
|
+
print("Fix: export OPENAI_API_KEY=sk-...")
|
|
185
|
+
elif "Local provider does not support" in err:
|
|
186
|
+
print("ERROR: Identity Provider not configured")
|
|
187
|
+
print("Fix: Console -> Settings -> Identity Bridge")
|
|
188
|
+
print("\nOr run simpler example first:")
|
|
189
|
+
print(" python openai_1a_dropin_simple.py")
|
|
190
|
+
else:
|
|
191
|
+
print(f"ERROR: {e}")
|
|
192
|
+
print("=" * 60)
|
|
193
|
+
sys.exit(1)
|