@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,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)
|