@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,193 @@
1
+ """
2
+ Native MCP endpoint for MACAW-registered tools.
3
+
4
+ Lets an ordinary MCP client (Claude Desktop, Cursor, mcp.ClientSession) talk to a
5
+ SecureMCP server or a SecureMCPProxy without knowing anything about MACAW.
6
+
7
+ Every JSON-RPC call is translated into the same invoke_tool() a MACAW-aware client
8
+ would make, so both entry paths land on the identical enforcement trunk:
9
+
10
+ external client --JSON-RPC--> serve() --+
11
+ +--> invoke_tool --> LocalAgent --> PEP --> tool
12
+ MACAW client ---------------------------+
13
+
14
+ The external caller has no MACAW identity of its own, so a stub agent
15
+ ("<name>-external") is registered to stand in for it. That stub is also where
16
+ server->client callbacks land: ctx.sample() / ctx.elicit() route back to it over the
17
+ bus, and it forwards them out over the live MCP session.
18
+ """
19
+
20
+ import asyncio
21
+ import json
22
+ import logging
23
+
24
+ import anyio
25
+ from mcp.server.lowlevel import Server
26
+ import mcp.types as types
27
+
28
+ from .client import Client
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def _as_text(result) -> str:
34
+ """Render a tool result for MCP.
35
+
36
+ MACAWClient wraps scalar returns as {"result": x}, so unwrap that one shape.
37
+ Anything else is a real payload from the tool and must survive intact - a tool
38
+ returning {"result": 13, "expression": "...", "history_count": 1} loses two
39
+ fields if we reach for ["result"].
40
+ """
41
+ if isinstance(result, dict):
42
+ if set(result.keys()) == {"result"}:
43
+ return str(result["result"])
44
+ return json.dumps(result, default=str)
45
+ return str(result)
46
+
47
+
48
+ async def serve(
49
+ name: str,
50
+ version: str,
51
+ registry,
52
+ target_agent: str,
53
+ prefix: str = "",
54
+ transport: str = "stdio",
55
+ host: str = "127.0.0.1",
56
+ port: int = 8080,
57
+ ) -> None:
58
+ """
59
+ Serve MACAW-registered tools over native MCP.
60
+
61
+ Args:
62
+ name: Server name (also names the stub caller agent)
63
+ version: Server version
64
+ registry: MACAWClient that holds the tools (the server's / proxy's own client).
65
+ Its .tools dict is the single source of truth for tools/list.
66
+ target_agent: Agent to invoke the tools on
67
+ prefix: MAPL name prefix. "" for SecureMCP, "tool:<app>/" for SecureMCPProxy.
68
+ Stripped on the way out, re-added on the way in.
69
+ transport: "stdio" or "http"
70
+ host: Bind host (http only)
71
+ port: Bind port (http only)
72
+ """
73
+ loop = asyncio.get_running_loop()
74
+ live = {} # holds the current MCP session for server->client callbacks
75
+
76
+ # Stub identity for callers that have no MACAW identity of their own.
77
+ caller = Client(f"{name}-external")
78
+
79
+ def _on_loop(coro):
80
+ # Callback handlers run in macaw_client's handler_pool thread, but the MCP
81
+ # session is bound to this event loop. Hand the coroutine back to it.
82
+ return asyncio.run_coroutine_threadsafe(coro, loop).result()
83
+
84
+ def _sample(prompt, system_prompt, max_tokens, temperature=None, **_kwargs):
85
+ # Forward temperature only if the calling tool asked for one. MCP carries
86
+ # it as an optional hint, and the client is free to ignore or reject it.
87
+ extra = {} if temperature is None else {"temperature": temperature}
88
+ result = _on_loop(
89
+ live["session"].create_message(
90
+ messages=[
91
+ types.SamplingMessage(
92
+ role="user",
93
+ content=types.TextContent(type="text", text=prompt),
94
+ )
95
+ ],
96
+ max_tokens=max_tokens,
97
+ system_prompt=system_prompt,
98
+ **extra,
99
+ )
100
+ )
101
+ return result.content.text
102
+
103
+ def _elicit(prompt, options, input_type, default, required, **_kwargs):
104
+ schema = {
105
+ "type": "object",
106
+ "required": ["value"] if required else [],
107
+ "properties": {
108
+ "value": {"type": "string", **({"enum": options} if options else {})}
109
+ },
110
+ }
111
+ result = _on_loop(live["session"].elicit(message=prompt, requestedSchema=schema))
112
+ if result.action != "accept" or not result.content:
113
+ return default
114
+ return result.content.get("value", default)
115
+
116
+ caller.set_sampling_handler(_sample)
117
+ caller.set_elicitation_handler(_elicit)
118
+
119
+ server = Server(name, version)
120
+
121
+ @server.list_tools()
122
+ async def _list_tools():
123
+ tools = []
124
+ for full_name, config in registry.tools.items():
125
+ if not full_name.startswith(prefix):
126
+ continue
127
+ bare = full_name[len(prefix):]
128
+ # Resources and prompts are registered as tools with these prefixes;
129
+ # they are not MCP tools. Internal callbacks are not either.
130
+ if bare.startswith(("resource:", "prompt:", "_mcp_")):
131
+ continue
132
+ tools.append(
133
+ types.Tool(
134
+ name=bare,
135
+ description=config.get("description", ""),
136
+ inputSchema=config.get("metadata", {}).get("schema")
137
+ or {"type": "object"},
138
+ )
139
+ )
140
+ return tools
141
+
142
+ @server.call_tool()
143
+ async def _call_tool(tool_name: str, arguments: dict):
144
+ ctx = server.request_context
145
+ live["session"] = ctx.session
146
+
147
+ peer = getattr(ctx.session.client_params, "clientInfo", None)
148
+ logger.info(
149
+ "tools/call %s from %s",
150
+ tool_name,
151
+ f"{peer.name}/{peer.version}" if peer else "unknown",
152
+ )
153
+
154
+ # Same call a MACAW client makes: policy, signing, and audit happen downstream.
155
+ #
156
+ # invoke_tool() is synchronous and blocks until the result comes back, so it
157
+ # MUST run off the event loop. If it blocks the loop, any tool that calls
158
+ # ctx.sample() / ctx.elicit() deadlocks: the handler routes the callback to
159
+ # our stub, whose handler needs this loop to send sampling/createMessage back
160
+ # over the session - and the loop is still blocked here waiting for the very
161
+ # invocation that callback belongs to. Both sides then time out.
162
+ result = await anyio.to_thread.run_sync(
163
+ lambda: caller.macaw_client.invoke_tool(
164
+ prefix + tool_name, arguments, target_agent=target_agent
165
+ )
166
+ )
167
+ return [types.TextContent(type="text", text=_as_text(result))]
168
+
169
+ init_options = server.create_initialization_options()
170
+
171
+ if transport == "stdio":
172
+ from mcp.server.stdio import stdio_server
173
+
174
+ logger.info("Serving MCP over stdio: %s", name)
175
+ async with stdio_server() as (read_stream, write_stream):
176
+ await server.run(read_stream, write_stream, init_options)
177
+
178
+ elif transport == "http":
179
+ from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
180
+ from starlette.applications import Starlette
181
+ from starlette.routing import Mount
182
+ import uvicorn
183
+
184
+ manager = StreamableHTTPSessionManager(app=server)
185
+ app = Starlette(routes=[Mount("/mcp", app=manager.handle_request)])
186
+
187
+ logger.info("Serving MCP over http: %s at http://%s:%s/mcp", name, host, port)
188
+ async with manager.run():
189
+ config = uvicorn.Config(app, host=host, port=port, log_level="info")
190
+ await uvicorn.Server(config).serve()
191
+
192
+ else:
193
+ raise ValueError(f"transport must be 'stdio' or 'http', got {transport!r}")
@@ -0,0 +1,225 @@
1
+ """
2
+ Native MCP endpoint for MACAW-registered tools - MCP 2026-07-28 binding.
3
+
4
+ Same contract and behaviour as _endpoint.py, written against the mcp>=2 SDK.
5
+ _serve.py picks between the two from the installed SDK version; nothing imports
6
+ this module directly.
7
+
8
+ What moved in the SDK (everything below the JSON-RPC boundary is unchanged):
9
+
10
+ _endpoint.py (mcp<2) _endpoint2.py (mcp>=2)
11
+ ------------------------------------ ------------------------------------
12
+ Server(name, version) version is keyword-only
13
+ @server.list_tools() -> list on_list_tools -> ListToolsResult
14
+ @server.call_tool() -> list on_call_tool -> CallToolResult
15
+ handler(tool_name, arguments) handler(ctx, params)
16
+ ctx = server.request_context ctx is a handler argument
17
+ Starlette + Mount + SessionManager server.streamable_http_app()
18
+
19
+ The MACAW half - tool filtering, invoke_tool, the stub caller, the sampling and
20
+ elicitation bridge - is the same logic as _endpoint.py.
21
+ """
22
+
23
+ import asyncio
24
+ import logging
25
+
26
+ import anyio
27
+ from mcp.server.caching import CacheHint
28
+ from mcp.server.lowlevel import Server
29
+ import mcp.types as types
30
+
31
+ from ._endpoint import _as_text
32
+ from .client import Client
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def _peer(ctx) -> str:
38
+ """Best-effort client identity for the log line.
39
+
40
+ 2026-07-28 is stateless: clients identify themselves per-request in _meta.
41
+ Legacy peers still arrive over a session with client_params, so check both.
42
+ """
43
+ meta = ctx.meta or {}
44
+ info = meta.get(types.CLIENT_INFO_META_KEY) if hasattr(meta, "get") else None
45
+ if info:
46
+ return f"{info.get('name', '?')}/{info.get('version', '?')}"
47
+
48
+ # A peer that negotiated down to an older revision identified itself in the
49
+ # initialize handshake instead, so it arrives on the session. mcp>=2 renamed
50
+ # that field clientInfo -> client_info.
51
+ params = getattr(getattr(ctx, "session", None), "client_params", None)
52
+ legacy = getattr(params, "client_info", None) or getattr(params, "clientInfo", None)
53
+ return f"{legacy.name}/{legacy.version}" if legacy else "unknown"
54
+
55
+
56
+ async def serve(
57
+ name: str,
58
+ version: str,
59
+ registry,
60
+ target_agent: str,
61
+ prefix: str = "",
62
+ transport: str = "stdio",
63
+ host: str = "127.0.0.1",
64
+ port: int = 8080,
65
+ ) -> None:
66
+ """
67
+ Serve MACAW-registered tools over native MCP. See _endpoint.serve().
68
+
69
+ Args:
70
+ name: Server name (also names the stub caller agent)
71
+ version: Server version
72
+ registry: MACAWClient that holds the tools (the server's / proxy's own client).
73
+ Its .tools dict is the single source of truth for tools/list.
74
+ target_agent: Agent to invoke the tools on
75
+ prefix: MAPL name prefix. "" for SecureMCP, "tool:<app>/" for SecureMCPProxy.
76
+ Stripped on the way out, re-added on the way in.
77
+ transport: "stdio" or "http"
78
+ host: Bind host (http only)
79
+ port: Bind port (http only)
80
+ """
81
+ loop = asyncio.get_running_loop()
82
+ live = {} # holds the current MCP session for server->client callbacks
83
+
84
+ # Stub identity for callers that have no MACAW identity of their own.
85
+ caller = Client(f"{name}-external")
86
+
87
+ def _on_loop(coro):
88
+ # Callback handlers run in macaw_client's handler_pool thread, but the MCP
89
+ # session is bound to this event loop. Hand the coroutine back to it.
90
+ return asyncio.run_coroutine_threadsafe(coro, loop).result()
91
+
92
+ def _sample(prompt, system_prompt, max_tokens, temperature=None, **_kwargs):
93
+ # Forward temperature only if the calling tool asked for one. MCP carries
94
+ # it as an optional hint, and the client is free to ignore or reject it.
95
+ #
96
+ # Sampling is deprecated as of 2026-07-28 (SEP-2577) and the SDK warns on
97
+ # this call. It still works for clients that support it; MACAW-native
98
+ # ctx.sample() over the mesh is unaffected either way.
99
+ extra = {} if temperature is None else {"temperature": temperature}
100
+ result = _on_loop(
101
+ live["session"].create_message(
102
+ messages=[
103
+ types.SamplingMessage(
104
+ role="user",
105
+ content=types.TextContent(type="text", text=prompt),
106
+ )
107
+ ],
108
+ max_tokens=max_tokens,
109
+ system_prompt=system_prompt,
110
+ **extra,
111
+ )
112
+ )
113
+ return result.content.text
114
+
115
+ def _elicit(prompt, options, input_type, default, required, **_kwargs):
116
+ schema = {
117
+ "type": "object",
118
+ "required": ["value"] if required else [],
119
+ "properties": {
120
+ "value": {"type": "string", **({"enum": options} if options else {})}
121
+ },
122
+ }
123
+ # 2026-07-28 renamed this parameter from requestedSchema.
124
+ result = _on_loop(
125
+ live["session"].elicit(message=prompt, requested_schema=schema)
126
+ )
127
+ if result.action != "accept" or not result.content:
128
+ return default
129
+ return result.content.get("value", default)
130
+
131
+ caller.set_sampling_handler(_sample)
132
+ caller.set_elicitation_handler(_elicit)
133
+
134
+ async def _list_tools(ctx, params) -> types.ListToolsResult:
135
+ tools = []
136
+ for full_name, config in registry.tools.items():
137
+ if not full_name.startswith(prefix):
138
+ continue
139
+ bare = full_name[len(prefix):]
140
+ # Resources and prompts are registered as tools with these prefixes;
141
+ # they are not MCP tools. Internal callbacks are not either.
142
+ if bare.startswith(("resource:", "prompt:", "_mcp_")):
143
+ continue
144
+ tools.append(
145
+ types.Tool(
146
+ name=bare,
147
+ description=config.get("description", ""),
148
+ inputSchema=config.get("metadata", {}).get("schema")
149
+ or {"type": "object"},
150
+ )
151
+ )
152
+ return types.ListToolsResult(tools=tools)
153
+
154
+ async def _call_tool(ctx, params) -> types.CallToolResult:
155
+ live["session"] = ctx.session
156
+
157
+ tool_name = params.name
158
+ arguments = params.arguments or {}
159
+ logger.info("tools/call %s from %s", tool_name, _peer(ctx))
160
+
161
+ # Same call a MACAW client makes: policy, signing, and audit happen downstream.
162
+ #
163
+ # invoke_tool() is synchronous and blocks until the result comes back, so it
164
+ # MUST run off the event loop. If it blocks the loop, any tool that calls
165
+ # ctx.sample() / ctx.elicit() deadlocks: the handler routes the callback to
166
+ # our stub, whose handler needs this loop to send sampling/createMessage back
167
+ # over the session - and the loop is still blocked here waiting for the very
168
+ # invocation that callback belongs to. Both sides then time out.
169
+ result = await anyio.to_thread.run_sync(
170
+ lambda: caller.macaw_client.invoke_tool(
171
+ prefix + tool_name, arguments, target_agent=target_agent
172
+ )
173
+ )
174
+ return types.CallToolResult(
175
+ content=[types.TextContent(type="text", text=_as_text(result))]
176
+ )
177
+
178
+ # Tool availability is an authorization statement, so a cached one is a stale
179
+ # one: ttl_ms=0 tells clients to re-ask, scope="private" forbids shared
180
+ # intermediaries from reusing an answer across principals. These are also the
181
+ # SDK's current defaults - stated here so a future default is our decision to
182
+ # change rather than something we inherit silently.
183
+ no_caching = {
184
+ "tools/list": CacheHint(ttl_ms=0, scope="private"),
185
+ "server/discover": CacheHint(ttl_ms=0, scope="private"),
186
+ }
187
+
188
+ server = Server(
189
+ name,
190
+ version=version,
191
+ cache_hints=no_caching,
192
+ on_list_tools=_list_tools,
193
+ on_call_tool=_call_tool,
194
+ )
195
+
196
+ if transport == "stdio":
197
+ from mcp.server.stdio import stdio_server
198
+
199
+ logger.info("Serving MCP over stdio: %s", name)
200
+ async with stdio_server() as (read_stream, write_stream):
201
+ await server.run(
202
+ read_stream, write_stream, server.create_initialization_options()
203
+ )
204
+
205
+ elif transport == "http":
206
+ import uvicorn
207
+
208
+ # streamable_http_app() builds the Starlette app and wires the session
209
+ # manager as its lifespan, so there is nothing to start by hand.
210
+ #
211
+ # stateless_http=True is the 2026-07-28 transport: no Mcp-Session-Id, every
212
+ # request self-describing. The SDK still defaults it to False for backward
213
+ # compatibility, but sessions are the thing MACAW never needed - identity,
214
+ # policy, and the signature travel with each invocation, not with a
215
+ # connection - so there is no cross-call state here to keep.
216
+ app = server.streamable_http_app(
217
+ streamable_http_path="/mcp", host=host, stateless_http=True
218
+ )
219
+
220
+ logger.info("Serving MCP over http: %s at http://%s:%s/mcp", name, host, port)
221
+ config = uvicorn.Config(app, host=host, port=port, log_level="info")
222
+ await uvicorn.Server(config).serve()
223
+
224
+ else:
225
+ raise ValueError(f"transport must be 'stdio' or 'http', got {transport!r}")
@@ -0,0 +1,25 @@
1
+ """
2
+ Pick the native MCP endpoint binding that matches the installed MCP SDK.
3
+
4
+ mcp 2.0 ships protocol revision 2026-07-28, which changed the server handler API
5
+ (see _endpoint2.py for the specifics). The two bindings present the same serve()
6
+ contract, so SecureMCP and SecureMCPProxy call this and stay version-agnostic:
7
+ the SDK you install decides which one runs.
8
+
9
+ pip install "mcp<2" -> _endpoint.py (2025-11-25 and earlier)
10
+ pip install "mcp>=2" -> _endpoint2.py (2026-07-28)
11
+ """
12
+
13
+ from importlib import import_module
14
+ from importlib.metadata import version
15
+
16
+
17
+ def _binding() -> str:
18
+ """Module name of the endpoint binding for the installed SDK."""
19
+ major = int(version("mcp").split(".")[0])
20
+ return "._endpoint2" if major >= 2 else "._endpoint"
21
+
22
+
23
+ async def serve(*args, **kwargs) -> None:
24
+ """Serve MACAW-registered tools over native MCP. See _endpoint.serve()."""
25
+ return await import_module(_binding(), __package__).serve(*args, **kwargs)