@macaw-ai/client 0.0.0-stage → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE.md +339 -0
  2. package/bin/install.js +278 -0
  3. package/package.json +37 -4
  4. package/payload/README.md +56 -0
  5. package/payload/secureAI/LICENSE +190 -0
  6. package/payload/secureAI/NOTICE +5 -0
  7. package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
  8. package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
  9. package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
  10. package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
  11. package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
  12. package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
  13. package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
  14. package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
  15. package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
  16. package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
  17. package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
  18. package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
  19. package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
  20. package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
  21. package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
  22. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
  23. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
  24. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
  25. package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
  26. package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
  27. package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
  28. package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
  29. package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
  30. package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
  31. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
  32. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
  33. package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
  34. package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
  35. package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
  36. package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
  37. package/payload/secureAI/examples/README.md +244 -0
  38. package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
  39. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
  40. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
  41. package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
  42. package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
  43. package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
  44. package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
  45. package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
  46. package/payload/secureAI/examples/attestations/README.md +128 -0
  47. package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
  48. package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
  49. package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
  50. package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
  51. package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
  52. package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
  53. package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
  54. package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
  55. package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
  56. package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
  57. package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
  58. package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
  59. package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
  60. package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
  61. package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
  62. package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
  63. package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
  64. package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
  65. package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
  66. package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
  67. package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
  68. package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
  69. package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
  70. package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
  71. package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
  72. package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
  73. package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
  74. package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
  75. package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
  76. package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
  77. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
  78. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
  79. package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
  80. package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
  81. package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
  82. package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
  83. package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
  84. package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
  85. package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
  86. package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
  87. package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
  88. package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
  89. package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
  90. package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
  91. package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
  92. package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
  93. package/payload/secureAI/macaw_adapters/__init__.py +55 -0
  94. package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
  95. package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
  96. package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
  97. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
  98. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
  99. package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
  100. package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
  101. package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
  102. package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
  103. package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
  104. package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
  105. package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
  106. package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
  107. package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
  108. package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
  109. package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
  110. package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
  111. package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
  112. package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
  113. package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
  114. package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
  115. package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
  116. package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
  117. package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
  118. package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
  119. package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
  120. package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
  121. package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
  122. package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
  123. package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
  124. package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
  125. package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
  126. package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
  127. package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
  128. package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
  129. package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
  130. package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
  131. package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
  132. package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
  133. package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
  134. package/payload/secureAI/test_harness_quick.py +296 -0
  135. package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
  136. package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
  137. package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
  138. package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
  139. package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
  140. package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
  141. package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
  142. package/README.md +0 -3
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Example: SecureMCPProxy with Databricks MCP Server (stdio transport)
4
+
5
+ Demonstrates SecureMCPProxy connecting to Databricks via stdio transport.
6
+ This uses the community databricks-mcp-server package.
7
+
8
+ Security Model:
9
+ - MACAW creates AuthenticatedContext for EACH invocation
10
+ - Even with persistent subprocess, each call gets fresh identity/policy/signature
11
+ - Databricks operations are policy-enforced and audited
12
+
13
+ Prerequisites:
14
+ 1. Databricks account with PAT token
15
+ 2. pip install databricks-mcp-server
16
+ 3. Environment variables:
17
+ export DATABRICKS_HOST="https://your-workspace.cloud.databricks.com"
18
+ export DATABRICKS_TOKEN="dapi..."
19
+ 4. MACAW: pip install macaw-adapters[mcp-proxy]
20
+
21
+ Usage:
22
+ python 2g_proxy_stdio_databricks.py
23
+ """
24
+
25
+ import os
26
+ import sys
27
+ import shutil
28
+ import logging
29
+
30
+ logging.basicConfig(level=logging.INFO)
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def main():
35
+ """Test SecureMCPProxy with Databricks MCP (stdio)."""
36
+ from macaw_adapters.mcp import SecureMCPProxy
37
+
38
+ print("=" * 60)
39
+ print("SecureMCPProxy + Databricks MCP (stdio)")
40
+ print("=" * 60)
41
+
42
+ # Step 1: Check prerequisites
43
+ print("\n1. Checking prerequisites...")
44
+
45
+ # Check uvx/databricks-mcp-server
46
+ if not shutil.which("uvx"):
47
+ print(" ERROR: uvx not found")
48
+ print(" Fix: pip install uv")
49
+ return 1
50
+ print(" OK: uvx found")
51
+
52
+ # Check environment variables
53
+ host = os.environ.get("DATABRICKS_HOST", "")
54
+ token = os.environ.get("DATABRICKS_TOKEN", "")
55
+
56
+ if not host or not token:
57
+ print(" ERROR: DATABRICKS_HOST and DATABRICKS_TOKEN required")
58
+ print(" Fix:")
59
+ print(' export DATABRICKS_HOST="https://your-workspace.cloud.databricks.com"')
60
+ print(' export DATABRICKS_TOKEN="dapi..."')
61
+ return 1
62
+ print(f" OK: DATABRICKS_HOST={host[:40]}...")
63
+ print(" OK: DATABRICKS_TOKEN=dapi***")
64
+
65
+ # Step 2: Create proxy with stdio transport
66
+ print("\n2. Creating SecureMCPProxy with Databricks stdio...")
67
+ try:
68
+ proxy = SecureMCPProxy(
69
+ app_name="databricks-stdio",
70
+ command=["uvx", "databricks-mcp-server@latest"],
71
+ env={
72
+ "DATABRICKS_HOST": host,
73
+ "DATABRICKS_TOKEN": token,
74
+ "HOME": os.environ.get("HOME", ""),
75
+ "PATH": os.environ.get("PATH", ""),
76
+ }
77
+ )
78
+ print(f" OK: {proxy}")
79
+ except ConnectionError as e:
80
+ print(f" ERROR: Failed to connect: {e}")
81
+ print("\n Troubleshooting:")
82
+ print(" 1. Install: pip install databricks-mcp-server")
83
+ print(" 2. Or run directly: uvx databricks-mcp-server@latest --help")
84
+ return 1
85
+ except Exception as e:
86
+ print(f" ERROR: {e}")
87
+ return 1
88
+
89
+ # Step 3: Discover tools
90
+ print("\n3. Discovering Databricks tools...")
91
+ tools = proxy.list_tools()
92
+ print(f" Found {len(tools)} tools:")
93
+ for tool in tools[:10]: # Show first 10
94
+ desc = tool.get('description', '')[:50]
95
+ print(f" - {tool['name']}: {desc}...")
96
+ if len(tools) > 10:
97
+ print(f" ... and {len(tools) - 10} more")
98
+
99
+ if not tools:
100
+ print(" ERROR: No tools discovered")
101
+ return 1
102
+
103
+ # Step 4: Test a tool call
104
+ print("\n4. Testing tool call...")
105
+
106
+ # Try to find a simple query tool
107
+ tool_names = [t['name'] for t in tools]
108
+
109
+ # Common tool names in databricks-mcp-server
110
+ test_tools = ['execute_query', 'run_query', 'list_catalogs', 'list_schemas']
111
+
112
+ for test_tool in test_tools:
113
+ if test_tool in tool_names:
114
+ print(f" Found tool: {test_tool}")
115
+ try:
116
+ if 'query' in test_tool:
117
+ result = proxy.call_tool(test_tool, {
118
+ "query": "SELECT current_user()"
119
+ })
120
+ else:
121
+ result = proxy.call_tool(test_tool, {})
122
+
123
+ content = result.get('result', result) if isinstance(result, dict) else result
124
+ preview = str(content)[:200]
125
+ print(f" OK: {preview}...")
126
+ break
127
+ except Exception as e:
128
+ print(f" Tool {test_tool} error: {e}")
129
+ else:
130
+ print(" Skipping tool call - no known test tool found")
131
+ print(f" Available: {tool_names[:5]}...")
132
+
133
+ print("\n" + "=" * 60)
134
+ print("SUCCESS: SecureMCPProxy Databricks stdio works!")
135
+ print("=" * 60)
136
+ print("\nKey points:")
137
+ print(" - stdio transport to databricks-mcp-server")
138
+ print(" - Same credentials as HTTP (DATABRICKS_HOST/TOKEN)")
139
+ print(" - MACAW policy enforcement on all calls")
140
+ return 0
141
+
142
+
143
+ if __name__ == "__main__":
144
+ sys.exit(main())
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 3a_native_mcp_client.py - A vanilla MCP client talking to SecureMCP
4
+
5
+ This client knows NOTHING about MACAW. It has no macaw imports, no agent
6
+ registration, no target_agent, no LocalAgent. It is the same code you would write
7
+ against any MCP server, and it is what Claude Desktop / Cursor do internally.
8
+
9
+ It still gets full MACAW enforcement, because the server's run(transport="stdio")
10
+ turns every JSON-RPC call into the same invoke_tool() a MACAW client would make:
11
+
12
+ this client --JSON-RPC--> calculator's MCP endpoint
13
+ |
14
+ +--> invoke_tool --> LocalAgent --> PEP --> add()
15
+ policy . signing . audit
16
+
17
+ Prerequisites:
18
+ - MACAW LocalAgent running
19
+ - pip install mcp
20
+
21
+ Run:
22
+ python 3a_native_mcp_client.py
23
+
24
+ (this spawns securemcp_calculator.py itself - no separate terminal needed)
25
+ """
26
+
27
+ import asyncio
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ from mcp import ClientSession, StdioServerParameters
32
+ from mcp.client.stdio import stdio_client
33
+
34
+ SERVER = Path(__file__).parent / "securemcp_calculator.py"
35
+
36
+
37
+ def field(obj, *names):
38
+ """Read an SDK field by whichever name this mcp version uses.
39
+
40
+ MCP 2026-07-28 renamed result fields from camelCase to snake_case
41
+ (serverInfo -> server_info). A real client targets one SDK and just uses
42
+ that spelling; this example runs against both.
43
+ """
44
+ return next((getattr(obj, n) for n in names if hasattr(obj, n)), None)
45
+
46
+
47
+ async def main() -> int:
48
+ print("=" * 60)
49
+ print("Vanilla MCP client -> SecureMCP calculator")
50
+ print("=" * 60)
51
+
52
+ params = StdioServerParameters(
53
+ command=sys.executable,
54
+ args=[str(SERVER), "stdio"],
55
+ )
56
+
57
+ async with stdio_client(params) as (read_stream, write_stream):
58
+ async with ClientSession(read_stream, write_stream) as session:
59
+
60
+ # 1. Handshake
61
+ init = await session.initialize()
62
+ info = field(init, "server_info", "serverInfo")
63
+ print(f"\nConnected to: {info.name} v{info.version}")
64
+ print(f"Protocol: {field(init, 'protocol_version', 'protocolVersion')}")
65
+
66
+ # 2. Discovery - comes straight off the MACAW tool registry
67
+ listed = await session.list_tools()
68
+ print(f"\nDiscovered {len(listed.tools)} tools:")
69
+ for tool in listed.tools:
70
+ schema = field(tool, "input_schema", "inputSchema") or {}
71
+ required = schema.get("required", [])
72
+ print(f" - {tool.name}({', '.join(required)}): {tool.description}")
73
+
74
+ # 3. Call tools - each one goes through the MACAW PEP
75
+ print("\nCalls (each policy-checked, signed, audited):")
76
+ for tool_name, args in [
77
+ ("add", {"a": 10, "b": 5}),
78
+ ("subtract", {"a": 100, "b": 42}),
79
+ ("multiply", {"a": 7, "b": 8}),
80
+ ("divide", {"a": 100, "b": 4}),
81
+ ]:
82
+ result = await session.call_tool(tool_name, args)
83
+ shown = ", ".join(f"{k}={v}" for k, v in args.items())
84
+ print(f" {tool_name}({shown}) = {result.content[0].text}")
85
+
86
+ # 4. A tool that uses Context (vault + audit) - works unchanged
87
+ result = await session.call_tool(
88
+ "calculate", {"operation": "add", "a": 10, "b": 3}
89
+ )
90
+ print(f" calculate(add, 10, 3) = {result.content[0].text}")
91
+
92
+ print("\n" + "=" * 60)
93
+ print("Done - and this file contains no MACAW code at all.")
94
+ print("=" * 60)
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ try:
100
+ sys.exit(asyncio.run(main()))
101
+ except Exception as e:
102
+ print(f"\nERROR: {e}")
103
+ print("\nCheck that the MACAW LocalAgent is running.")
104
+ sys.exit(1)
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 3b_native_mcp_proxy.py - A vanilla MCP client talking THROUGH SecureMCPProxy
4
+
5
+ The "we don't have the code" case. mcp_server_fetch is a third-party MCP server
6
+ we don't control. Put it behind SecureMCPProxy and every call is policy-checked,
7
+ signed, and audited - and the client can't tell the difference:
8
+
9
+ this client --JSON-RPC--> proxy.run()
10
+ |
11
+ +--> invoke_tool --> LocalAgent --> PEP
12
+ |
13
+ proxy handler --> mcp_server_fetch
14
+ (stdio) |
15
+ the web
16
+
17
+ This is the drop-in story: in a Claude Desktop config you point at this proxy
18
+ instead of at mcp_server_fetch, and nothing else changes.
19
+
20
+ Prerequisites:
21
+ - MACAW LocalAgent running
22
+ - pip install mcp mcp-server-fetch
23
+ - Policy allowing: tool:fetch-test/fetch
24
+
25
+ Run:
26
+ python 3b_native_mcp_proxy.py
27
+
28
+ (this spawns the proxy itself - no separate terminal needed)
29
+ """
30
+
31
+ import asyncio
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ from mcp import ClientSession, StdioServerParameters
36
+ from mcp.client.stdio import stdio_client
37
+
38
+
39
+ def field(obj, *names):
40
+ """Read an SDK field by whichever name this mcp version uses.
41
+
42
+ MCP 2026-07-28 renamed result fields from camelCase to snake_case
43
+ (serverInfo -> server_info). A real client targets one SDK and just uses
44
+ that spelling; this example runs against both.
45
+ """
46
+ return next((getattr(obj, n) for n in names if hasattr(obj, n)), None)
47
+
48
+
49
+ APP_NAME = "fetch-test"
50
+
51
+ # The proxy process this client spawns. Two lines of real code:
52
+ # SecureMCPProxy(app_name=..., command=[...]).run()
53
+ # In a Claude Desktop config, this is what "command"/"args" would point at.
54
+ RUNNER = f'''
55
+ import sys
56
+ from macaw_adapters.mcp import SecureMCPProxy
57
+ SecureMCPProxy(
58
+ app_name="{APP_NAME}",
59
+ command=[sys.executable, "-m", "mcp_server_fetch"],
60
+ ).run(transport="stdio")
61
+ '''
62
+
63
+
64
+ async def main() -> int:
65
+ print("=" * 60)
66
+ print("Vanilla MCP client -> SecureMCPProxy -> mcp_server_fetch")
67
+ print("=" * 60)
68
+
69
+ params = StdioServerParameters(command=sys.executable, args=["-c", RUNNER])
70
+
71
+ async with stdio_client(params) as (read_stream, write_stream):
72
+ async with ClientSession(read_stream, write_stream) as session:
73
+
74
+ init = await session.initialize()
75
+ print(f"\nConnected to: {field(init, 'server_info', 'serverInfo').name}")
76
+
77
+ # Discovery passes upstream's schema through untouched - the proxy
78
+ # never had to know what a "fetch" tool looks like.
79
+ listed = await session.list_tools()
80
+ print(f"\nDiscovered {len(listed.tools)} tools (schemas from upstream):")
81
+ for tool in listed.tools:
82
+ schema = field(tool, "input_schema", "inputSchema") or {}
83
+ props = list(schema.get("properties", {}).keys())
84
+ print(f" - {tool.name}({', '.join(props)})")
85
+ print(f" {(tool.description or '')[:70]}...")
86
+
87
+ print("\nCall (policy-checked, signed, audited, then forwarded upstream):")
88
+ print("-" * 60)
89
+ result = await session.call_tool("fetch", {"url": "https://example.com"})
90
+ preview = result.content[0].text[:180].replace("\n", " ")
91
+ print(f" fetch(https://example.com) -> {preview}...")
92
+
93
+ print("\n" + "=" * 60)
94
+ print("Done - upstream is untouched, this client has no MACAW code,")
95
+ print("and MACAW enforced every call in between.")
96
+ print("=" * 60)
97
+ return 0
98
+
99
+
100
+ if __name__ == "__main__":
101
+ try:
102
+ import mcp_server_fetch # noqa: F401
103
+ except ImportError:
104
+ print("ERROR: mcp-server-fetch not installed")
105
+ print("Fix: pip install mcp-server-fetch")
106
+ sys.exit(1)
107
+
108
+ try:
109
+ sys.exit(asyncio.run(main()))
110
+ except Exception as e:
111
+ print(f"\nERROR: {e}")
112
+ print("\nCheck that the MACAW LocalAgent is running, and that policy")
113
+ print(f"allows: tool:{APP_NAME}/fetch")
114
+ sys.exit(1)
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 3c_native_mcp_sampling.py - MCP Sampling through the native endpoint
4
+
5
+ The duplex test. A vanilla MCP client (no MACAW code) calls a SecureMCP tool,
6
+ and that tool calls back OUT to this client's LLM via ctx.sample():
7
+
8
+ this client --tools/call--> sampling-demo's MCP endpoint
9
+ |
10
+ +--> invoke_tool --> PEP --> summarize()
11
+ |
12
+ ctx.sample()
13
+ |
14
+ +--- invoke_tool("_mcp_sample") <-------+
15
+ | (over the MACAW bus)
16
+ v
17
+ the endpoint's stub agent
18
+ |
19
+ +--- sampling/createMessage --> this client's callback
20
+ |
21
+ Anthropic API
22
+ |
23
+ summary <---------------------------------------------------------+
24
+
25
+ Both directions cross the boundary: the request goes in over JSON-RPC and down
26
+ through the MACAW PEP; the LLM callback comes back out over the same JSON-RPC
27
+ session. This is the same _mcp_sample mechanism 1e_sampling_client.py uses over
28
+ the bus - the only difference is that the stub forwards it over MCP instead of
29
+ calling an LLM directly.
30
+
31
+ Prerequisites:
32
+ - MACAW LocalAgent running
33
+ - pip install mcp anthropic
34
+ - ANTHROPIC_API_KEY set (this is the client's LLM - the whole point of sampling)
35
+
36
+ Run:
37
+ export ANTHROPIC_API_KEY=sk-ant-...
38
+ python 3c_native_mcp_sampling.py
39
+
40
+ (this spawns 1e_sampling_server.py itself - no separate terminal needed)
41
+ """
42
+
43
+ import asyncio
44
+ import os
45
+ import sys
46
+ from pathlib import Path
47
+
48
+ import anthropic
49
+ from mcp import ClientSession, StdioServerParameters
50
+ from mcp.client.stdio import stdio_client
51
+ import mcp.types as types
52
+
53
+ SERVER = Path(__file__).parent / "1e_sampling_server.py"
54
+ MODEL = "claude-opus-4-8"
55
+
56
+
57
+ def field(obj, *names):
58
+ """Read an SDK field by whichever name this mcp version uses.
59
+
60
+ MCP 2026-07-28 renamed result fields from camelCase to snake_case
61
+ (serverInfo -> server_info). A real client targets one SDK and just uses
62
+ that spelling; this example runs against both.
63
+ """
64
+ return next((getattr(obj, n) for n in names if hasattr(obj, n)), None)
65
+
66
+ llm = anthropic.Anthropic()
67
+
68
+
69
+ async def sampling_callback(
70
+ # The callback shape is (context, params) on every mcp version; only the
71
+ # context type moved (RequestContext on mcp<2, ClientRequestContext on
72
+ # mcp>=2), and it is unused here, so it is left unannotated.
73
+ context,
74
+ params: types.CreateMessageRequestParams,
75
+ ) -> types.CreateMessageResult | types.ErrorData:
76
+ """Serve the server's ctx.sample() request using THIS client's LLM.
77
+
78
+ This is the client half of MCP sampling: the server has no model of its own,
79
+ so it borrows ours. Nothing here knows about MACAW.
80
+ """
81
+ prompt = "\n".join(
82
+ block.text for m in params.messages
83
+ if (block := m.content).type == "text"
84
+ )
85
+ print(f" [LLM] <- server asked: {prompt[:70]}...")
86
+
87
+ def call(**extra):
88
+ return llm.messages.create(
89
+ model=MODEL,
90
+ max_tokens=field(params, "max_tokens", "maxTokens"),
91
+ system=field(params, "system_prompt", "systemPrompt")
92
+ or "You are a helpful assistant.",
93
+ messages=[{"role": "user", "content": prompt}],
94
+ **extra,
95
+ )
96
+
97
+ # Forward a temperature only if the tool asked for one, and don't assume whether
98
+ # this model takes it - try, and fall back if it doesn't. Current Claude models
99
+ # (Opus 4.8/4.7, Sonnet 5, Fable 5) reject temperature/top_p/top_k with a 400;
100
+ # older ones accept it. MCP treats sampling params as hints a client may ignore,
101
+ # so degrading beats failing the call over one.
102
+ extra = {} if params.temperature is None else {"temperature": params.temperature}
103
+ try:
104
+ try:
105
+ response = call(**extra)
106
+ except anthropic.BadRequestError:
107
+ if not extra:
108
+ raise
109
+ print(f" [LLM] {MODEL} rejected temperature={params.temperature}; retrying without")
110
+ response = call()
111
+ except anthropic.APIStatusError as e:
112
+ return types.ErrorData(code=types.INTERNAL_ERROR, message=str(e))
113
+
114
+ text = next((b.text for b in response.content if b.type == "text"), "")
115
+ print(f" [LLM] -> answered: {text[:70]}...")
116
+
117
+ return types.CreateMessageResult(
118
+ role="assistant",
119
+ content=types.TextContent(type="text", text=text),
120
+ model=response.model,
121
+ stopReason="endTurn",
122
+ )
123
+
124
+
125
+ async def main() -> int:
126
+ print("=" * 60)
127
+ print("Vanilla MCP client + sampling -> SecureMCP")
128
+ print("=" * 60)
129
+
130
+ params = StdioServerParameters(command=sys.executable, args=[str(SERVER), "stdio"])
131
+
132
+ async with stdio_client(params) as (read_stream, write_stream):
133
+ async with ClientSession(
134
+ read_stream, write_stream, sampling_callback=sampling_callback
135
+ ) as session:
136
+ init = await session.initialize()
137
+ print(f"\nConnected to: {field(init, 'server_info', 'serverInfo').name}")
138
+
139
+ listed = await session.list_tools()
140
+ print(f"Tools: {[t.name for t in listed.tools]}\n")
141
+
142
+ print("summarize() - the tool will call back to our LLM:")
143
+ print("-" * 60)
144
+ result = await session.call_tool("summarize", {
145
+ "text": (
146
+ "MACAW places a policy enforcement point between an agent and the "
147
+ "tools it calls. Every invocation is checked against policy, signed, "
148
+ "and written to an audit log, so a non-deterministic model still "
149
+ "produces deterministic, reviewable actions."
150
+ ),
151
+ "max_length": 40,
152
+ })
153
+ print(f" RESULT: {result.content[0].text}\n")
154
+
155
+ print("analyze_sentiment() - same round trip:")
156
+ print("-" * 60)
157
+ result = await session.call_tool("analyze_sentiment", {
158
+ "text": "I love this product! It's amazing and works perfectly!"
159
+ })
160
+ print(f" RESULT: {result.content[0].text}")
161
+
162
+ print("\n" + "=" * 60)
163
+ print("Done - request in over MCP, LLM callback back out over MCP,")
164
+ print("policy and audit in the middle. No MACAW code in this file.")
165
+ print("=" * 60)
166
+ return 0
167
+
168
+
169
+ if __name__ == "__main__":
170
+ if not os.environ.get("ANTHROPIC_API_KEY"):
171
+ print("ERROR: ANTHROPIC_API_KEY is not set.")
172
+ print("Sampling means the SERVER borrows THIS client's LLM, so a real key")
173
+ print("is required - there is no mock path in this example by design.")
174
+ sys.exit(1)
175
+ try:
176
+ sys.exit(asyncio.run(main()))
177
+ except Exception as e:
178
+ print(f"\nERROR: {e}")
179
+ print("\nCheck that the MACAW LocalAgent is running.")
180
+ sys.exit(1)
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Example 3d: MCP 2026-07-28 by hand - no client library at all.
4
+
5
+ MCP revision 2026-07-28 made the protocol stateless. There is no initialize
6
+ handshake and no Mcp-Session-Id: every request carries its own protocol version
7
+ and client capabilities in _meta, and servers answer server/discover to advertise
8
+ what they speak.
9
+
10
+ That is a good fit for MACAW, because MACAW never had connection state to lose.
11
+ Identity, policy, and the signature travel with each invocation, not with a
12
+ session - which is exactly what the spec moved toward.
13
+
14
+ This client is plain HTTP. No mcp package, no ClientSession, no handshake:
15
+
16
+ this file --POST /mcp--> SecureMCP --invoke_tool--> LocalAgent --> PEP --> tool
17
+
18
+ Every call below is still policy-checked, signed, and audited, because the wire
19
+ format has nothing to do with where enforcement happens.
20
+
21
+ Run:
22
+ # terminal 1 - requires mcp>=2
23
+ python3 securemcp_calculator.py http
24
+
25
+ # terminal 2
26
+ python3 3d_native_mcp_2026.py
27
+ """
28
+
29
+ import json
30
+ import sys
31
+ import urllib.error
32
+ import urllib.request
33
+
34
+ URL = "http://127.0.0.1:8080/mcp"
35
+ PROTOCOL = "2026-07-28"
36
+
37
+ # Reserved _meta keys. Every request self-describes; there is no handshake to
38
+ # establish them once.
39
+ PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion"
40
+ CLIENT_INFO = "io.modelcontextprotocol/clientInfo"
41
+ CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities"
42
+
43
+
44
+ def rpc(method: str, params: dict | None = None, name: str | None = None) -> dict:
45
+ """One stateless JSON-RPC call. No session, no prior state."""
46
+ body = {
47
+ "jsonrpc": "2.0",
48
+ "id": 1,
49
+ "method": method,
50
+ "params": {
51
+ **(params or {}),
52
+ "_meta": {
53
+ PROTOCOL_VERSION: PROTOCOL,
54
+ CLIENT_INFO: {"name": "raw-http-demo", "version": "1.0"},
55
+ CLIENT_CAPABILITIES: {},
56
+ },
57
+ },
58
+ }
59
+ headers = {
60
+ "Content-Type": "application/json",
61
+ "Accept": "application/json, text/event-stream",
62
+ # Selects the 2026-07-28 era. Without it the server answers the older
63
+ # session-based revision for backward compatibility.
64
+ "MCP-Protocol-Version": PROTOCOL,
65
+ "Mcp-Method": method,
66
+ }
67
+ if name:
68
+ headers["Mcp-Name"] = name
69
+
70
+ req = urllib.request.Request(URL, json.dumps(body).encode(), headers)
71
+ with urllib.request.urlopen(req, timeout=30) as resp:
72
+ session_id = resp.headers.get("mcp-session-id")
73
+ text = resp.read().decode()
74
+
75
+ # Responses may arrive as a one-event SSE stream.
76
+ if text.startswith("event:"):
77
+ text = "".join(l[6:] for l in text.splitlines() if l.startswith("data: "))
78
+
79
+ payload = json.loads(text)
80
+ if "error" in payload:
81
+ raise RuntimeError(f"{method}: {payload['error']}")
82
+ return {"result": payload["result"], "session_id": session_id}
83
+
84
+
85
+ def main() -> int:
86
+ print("=" * 60)
87
+ print(f"Raw HTTP client -> SecureMCP, protocol {PROTOCOL}")
88
+ print("=" * 60)
89
+
90
+ # 1. Discovery. Replaces the initialize handshake: ask what the server speaks.
91
+ d = rpc("server/discover")
92
+ discover, session_id = d["result"], d["session_id"]
93
+ print(f"\nserver/discover (session id: {session_id or 'none - stateless'})")
94
+ print(f" supportedVersions : {discover['supportedVersions']}")
95
+ print(f" capabilities : {discover['capabilities']}")
96
+ print(f" serverInfo : {discover['_meta'][ 'io.modelcontextprotocol/serverInfo']}")
97
+
98
+ # 2. Tools. ttlMs/cacheScope are the server's caching contract with the client.
99
+ listed = rpc("tools/list")["result"]
100
+ print("\ntools/list")
101
+ print(f" resultType : {listed['resultType']}")
102
+ print(f" ttlMs / cacheScope: {listed['ttlMs']} / {listed['cacheScope']}")
103
+ print(" ttlMs=0 means do not cache. Which tools exist is an authorization")
104
+ print(" answer, and a cached authorization answer is a stale one.")
105
+ print(f" tools : {', '.join(t['name'] for t in listed['tools'])}")
106
+
107
+ # 3. Calls. Each one is a fresh, self-describing request - and each one is
108
+ # policy-checked, signed, and audited by MACAW on the way through.
109
+ print("\ntools/call (each policy-checked, signed, audited)")
110
+ for tool, args in [
111
+ ("add", {"a": 10, "b": 5}),
112
+ ("divide", {"a": 100, "b": 4}),
113
+ ("calculate", {"operation": "add", "a": 10, "b": 3}),
114
+ ]:
115
+ out = rpc("tools/call", {"name": tool, "arguments": args}, name=tool)["result"]
116
+ shown = ", ".join(f"{k}={v}" for k, v in args.items())
117
+ print(f" {tool}({shown}) = {out['content'][0]['text']}")
118
+
119
+ print("\n" + "=" * 60)
120
+ print("No handshake, no session, no MCP library - and still fully enforced.")
121
+ print("=" * 60)
122
+ return 0
123
+
124
+
125
+ if __name__ == "__main__":
126
+ try:
127
+ sys.exit(main())
128
+ except urllib.error.URLError as e:
129
+ print(f"\nERROR: cannot reach {URL} ({e})")
130
+ print("Start the server first: python3 securemcp_calculator.py http")
131
+ print("2026-07-28 requires mcp>=2 - check with: pip show mcp")
132
+ sys.exit(1)
133
+ except RuntimeError as e:
134
+ print(f"\nERROR: {e}")
135
+ sys.exit(1)