managed-deepagents 0.6.2.dev7__py3-none-win_amd64.whl → 0.6.2.dev9__py3-none-win_amd64.whl
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.
- managed_deepagents/__init__.py +1 -13
- managed_deepagents/_binaries/mda.exe +0 -0
- managed_deepagents/_channels/schedule_delivery.py +59 -12
- managed_deepagents/_channels/trigger/client.py +37 -9
- managed_deepagents/_channels/trigger/types.py +5 -2
- managed_deepagents/_connectors/mcp/__init__.py +2 -12
- managed_deepagents/_connectors/mcp/load.py +6 -172
- managed_deepagents/_identity_auth.py +1 -1
- managed_deepagents/_identity_runtime.py +7 -310
- managed_deepagents/_managed_middleware.py +9 -10
- managed_deepagents/_managed_tools.py +23 -20
- managed_deepagents/identity/__init__.py +5 -58
- managed_deepagents/runtime.py +4 -31
- {managed_deepagents-0.6.2.dev7.dist-info → managed_deepagents-0.6.2.dev9.dist-info}/METADATA +1 -1
- {managed_deepagents-0.6.2.dev7.dist-info → managed_deepagents-0.6.2.dev9.dist-info}/RECORD +19 -23
- managed_deepagents/_agent_auth.py +0 -260
- managed_deepagents/_agent_auth_http.py +0 -119
- managed_deepagents/_credential_gate.py +0 -372
- managed_deepagents/_credentials.py +0 -417
- {managed_deepagents-0.6.2.dev7.dist-info → managed_deepagents-0.6.2.dev9.dist-info}/WHEEL +0 -0
- {managed_deepagents-0.6.2.dev7.dist-info → managed_deepagents-0.6.2.dev9.dist-info}/entry_points.txt +0 -0
- {managed_deepagents-0.6.2.dev7.dist-info → managed_deepagents-0.6.2.dev9.dist-info}/licenses/LICENSE +0 -0
- {managed_deepagents-0.6.2.dev7.dist-info → managed_deepagents-0.6.2.dev9.dist-info}/top_level.txt +0 -0
managed_deepagents/__init__.py
CHANGED
|
@@ -9,19 +9,12 @@ This package provides two things:
|
|
|
9
9
|
|
|
10
10
|
Resources use ``define_*`` factories while provider collections use namespaces:
|
|
11
11
|
``define_sandbox()``, ``channels.slack(...)``, ``connectors.mcp(...)``,
|
|
12
|
-
|
|
12
|
+
and ``auth.supabase(...)``. Contributing a *new*
|
|
13
13
|
primitive is not part of the public surface.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
from managed_deepagents._credentials import (
|
|
19
|
-
CredentialAuthorizationRequiredError,
|
|
20
|
-
CredentialNotFoundError,
|
|
21
|
-
CredentialOwner,
|
|
22
|
-
CredentialStoreError,
|
|
23
|
-
credentials,
|
|
24
|
-
)
|
|
25
18
|
from managed_deepagents._managed_tools import (
|
|
26
19
|
ManagedDeepAgentRuntime,
|
|
27
20
|
)
|
|
@@ -38,10 +31,6 @@ from managed_deepagents.sandbox import SandboxDefinition
|
|
|
38
31
|
from managed_deepagents.schedules import define_schedule
|
|
39
32
|
|
|
40
33
|
__all__ = [
|
|
41
|
-
"CredentialAuthorizationRequiredError",
|
|
42
|
-
"CredentialNotFoundError",
|
|
43
|
-
"CredentialOwner",
|
|
44
|
-
"CredentialStoreError",
|
|
45
34
|
"DeepAgentDefinition",
|
|
46
35
|
"ManagedDeepAgentRuntime",
|
|
47
36
|
"MemoryDefinition",
|
|
@@ -49,7 +38,6 @@ __all__ = [
|
|
|
49
38
|
"auth",
|
|
50
39
|
"channels",
|
|
51
40
|
"connectors",
|
|
52
|
-
"credentials",
|
|
53
41
|
"define_deep_agent",
|
|
54
42
|
"define_identity",
|
|
55
43
|
"define_memory",
|
|
Binary file
|
|
@@ -2,30 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import logging
|
|
6
5
|
import os
|
|
6
|
+
import uuid
|
|
7
7
|
from collections.abc import Awaitable, Callable, Mapping
|
|
8
8
|
from importlib import import_module
|
|
9
9
|
from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
|
|
10
10
|
|
|
11
11
|
from managed_deepagents._channels.assistant_text import extract_assistant_text
|
|
12
12
|
from managed_deepagents._channels.binding import SLACK_BOT_TOKEN_ENV
|
|
13
|
+
from managed_deepagents._channels.trigger.client import create_trigger_client
|
|
13
14
|
from managed_deepagents._identity_runtime import (
|
|
14
15
|
_ambient_configurable,
|
|
15
16
|
_configurable_from_runtime,
|
|
16
17
|
)
|
|
18
|
+
from managed_deepagents._log import logger
|
|
17
19
|
from managed_deepagents._memory.async_util import await_if_needed
|
|
18
20
|
|
|
19
21
|
if TYPE_CHECKING:
|
|
20
22
|
from langchain.agents.middleware.types import AgentMiddleware
|
|
21
23
|
|
|
24
|
+
from managed_deepagents._channels.trigger.types import TriggerChannelActionRequest
|
|
22
25
|
from managed_deepagents._channels.types import ChannelAddress
|
|
23
26
|
else:
|
|
24
27
|
# Authoring-light installs may lack langchain; annotations stay deferred.
|
|
25
28
|
AgentMiddleware = object
|
|
26
29
|
|
|
27
|
-
_LOG = logging.getLogger("mda")
|
|
28
|
-
|
|
29
30
|
_schedule_metadata_for_tests: Callable[[], Mapping[str, object] | None] | None = None
|
|
30
31
|
|
|
31
32
|
|
|
@@ -58,6 +59,16 @@ class NormalizedScheduleDeliverTo(TypedDict):
|
|
|
58
59
|
auto_post: bool
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
def get_deployment_id(env: Mapping[str, str] | None = None) -> str | None:
|
|
63
|
+
"""Return the deployment ID supplied by the managed runtime."""
|
|
64
|
+
resolved_env = os.environ if env is None else env
|
|
65
|
+
for key in ("LANGSMITH_HOST_PROJECT_ID", "LANGGRAPH_CLOUD_ID"):
|
|
66
|
+
value = (resolved_env.get(key) or "").strip()
|
|
67
|
+
if value:
|
|
68
|
+
return value
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
61
72
|
def read_schedule_deliver_to( # noqa: PLR0911 - validation short-circuits
|
|
62
73
|
configurable: Mapping[str, object] | None,
|
|
63
74
|
) -> NormalizedScheduleDeliverTo | None:
|
|
@@ -122,7 +133,7 @@ def address_from_deliver_to(to: _ProviderConversation | _ProviderThread) -> Chan
|
|
|
122
133
|
return {"conversation_id": to["conversation_id"]}
|
|
123
134
|
|
|
124
135
|
|
|
125
|
-
async def deliver_schedule_result( # noqa: PLR0911 - soft-fail outcomes
|
|
136
|
+
async def deliver_schedule_result( # noqa: C901, PLR0911 - soft-fail outcomes
|
|
126
137
|
*,
|
|
127
138
|
deliver_to: NormalizedScheduleDeliverTo,
|
|
128
139
|
reply_text: str,
|
|
@@ -134,9 +145,9 @@ async def deliver_schedule_result( # noqa: PLR0911 - soft-fail outcomes
|
|
|
134
145
|
if not text or not deliver_to["auto_post"]:
|
|
135
146
|
return "skipped"
|
|
136
147
|
if deliver_to["channel"] != "slack":
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
deliver_to["channel"],
|
|
148
|
+
logger.error(
|
|
149
|
+
'schedule deliver_to channel "%s" is not supported yet',
|
|
150
|
+
logger.log_identifier(deliver_to["channel"]),
|
|
140
151
|
)
|
|
141
152
|
return "failed"
|
|
142
153
|
|
|
@@ -151,15 +162,47 @@ async def deliver_schedule_result( # noqa: PLR0911 - soft-fail outcomes
|
|
|
151
162
|
if hasattr(result, "__await__"):
|
|
152
163
|
await cast("Awaitable[object]", result)
|
|
153
164
|
return "posted"
|
|
154
|
-
except Exception: # noqa: BLE001 - soft-fail schedule delivery
|
|
155
|
-
|
|
165
|
+
except Exception as error: # noqa: BLE001 - soft-fail schedule delivery
|
|
166
|
+
logger.error(
|
|
167
|
+
"schedule Slack delivery failed (%s)",
|
|
168
|
+
logger.safe_error_label(error),
|
|
169
|
+
)
|
|
170
|
+
return "failed"
|
|
171
|
+
|
|
172
|
+
deployment_id = get_deployment_id()
|
|
173
|
+
try:
|
|
174
|
+
client = create_trigger_client()
|
|
175
|
+
except ValueError:
|
|
176
|
+
client = None
|
|
177
|
+
if client is not None and deployment_id:
|
|
178
|
+
action: TriggerChannelActionRequest = {
|
|
179
|
+
"type": "post",
|
|
180
|
+
"action_id": str(uuid.uuid4()),
|
|
181
|
+
"address": dict(address),
|
|
182
|
+
"message": {"text": text},
|
|
183
|
+
}
|
|
184
|
+
try:
|
|
185
|
+
await client.post_deployment_action(
|
|
186
|
+
deployment_id,
|
|
187
|
+
deliver_to["channel"],
|
|
188
|
+
action,
|
|
189
|
+
)
|
|
190
|
+
return "posted"
|
|
191
|
+
except Exception as error: # noqa: BLE001 - soft-fail schedule delivery
|
|
192
|
+
logger.error(
|
|
193
|
+
"schedule Trigger channel delivery failed (%s)",
|
|
194
|
+
logger.safe_error_label(error),
|
|
195
|
+
)
|
|
156
196
|
return "failed"
|
|
157
197
|
|
|
158
198
|
token = (
|
|
159
199
|
bot_token if bot_token is not None else os.environ.get(SLACK_BOT_TOKEN_ENV) or ""
|
|
160
200
|
).strip()
|
|
161
201
|
if not token:
|
|
162
|
-
|
|
202
|
+
logger.error(
|
|
203
|
+
"schedule deliver_to requires Trigger channel configuration or "
|
|
204
|
+
"SLACK_BOT_TOKEN; skipping auto-post"
|
|
205
|
+
)
|
|
163
206
|
return "failed"
|
|
164
207
|
|
|
165
208
|
try:
|
|
@@ -176,8 +219,11 @@ async def deliver_schedule_result( # noqa: PLR0911 - soft-fail outcomes
|
|
|
176
219
|
if hasattr(posted, "__await__"):
|
|
177
220
|
await cast("Awaitable[object]", posted)
|
|
178
221
|
return "posted"
|
|
179
|
-
except Exception: # noqa: BLE001 - soft-fail schedule delivery
|
|
180
|
-
|
|
222
|
+
except Exception as error: # noqa: BLE001 - soft-fail schedule delivery
|
|
223
|
+
logger.error(
|
|
224
|
+
"schedule Slack delivery failed (%s)",
|
|
225
|
+
logger.safe_error_label(error),
|
|
226
|
+
)
|
|
181
227
|
return "failed"
|
|
182
228
|
|
|
183
229
|
|
|
@@ -297,6 +343,7 @@ __all__ = [
|
|
|
297
343
|
"address_from_deliver_to",
|
|
298
344
|
"create_schedule_delivery_middleware",
|
|
299
345
|
"deliver_schedule_result",
|
|
346
|
+
"get_deployment_id",
|
|
300
347
|
"is_trusted_schedule_delivery_run",
|
|
301
348
|
"read_schedule_deliver_to",
|
|
302
349
|
]
|
|
@@ -93,18 +93,41 @@ class TriggerClient(TriggerClientLike):
|
|
|
93
93
|
channel_id: str,
|
|
94
94
|
body: TriggerChannelActionRequest,
|
|
95
95
|
) -> TriggerChannelActionResponse:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
encoded_channel_id = urllib.parse.quote(channel_id, safe="")
|
|
97
|
+
response = await self._post(
|
|
98
|
+
"actions",
|
|
99
|
+
f"{self._api_base}/channels/{encoded_channel_id}/actions",
|
|
100
|
+
body,
|
|
101
|
+
)
|
|
102
|
+
return _parse_action_response(response)
|
|
103
|
+
|
|
104
|
+
async def post_deployment_action(
|
|
105
|
+
self,
|
|
106
|
+
deployment_id: str,
|
|
107
|
+
channel_name: str,
|
|
108
|
+
body: TriggerChannelActionRequest,
|
|
109
|
+
) -> TriggerChannelActionResponse:
|
|
110
|
+
query = urllib.parse.urlencode(
|
|
111
|
+
{"deployment_id": deployment_id, "channel_name": channel_name}
|
|
112
|
+
)
|
|
113
|
+
response = await self._post(
|
|
114
|
+
"actions",
|
|
115
|
+
f"{self._api_base}/channels/actions?{query}",
|
|
116
|
+
body,
|
|
117
|
+
)
|
|
118
|
+
return _parse_action_response(response)
|
|
101
119
|
|
|
102
120
|
async def post_runtime_event(
|
|
103
121
|
self,
|
|
104
122
|
channel_id: str,
|
|
105
123
|
body: TriggerChannelRuntimeEventRequest,
|
|
106
124
|
) -> TriggerChannelRuntimeEventResponse:
|
|
107
|
-
|
|
125
|
+
encoded_channel_id = urllib.parse.quote(channel_id, safe="")
|
|
126
|
+
response = await self._post(
|
|
127
|
+
"events",
|
|
128
|
+
f"{self._api_base}/channels/{encoded_channel_id}/events",
|
|
129
|
+
body,
|
|
130
|
+
)
|
|
108
131
|
status = response.get("status") if isinstance(response, dict) else None
|
|
109
132
|
if not isinstance(status, str):
|
|
110
133
|
raise TriggerClientError("events")
|
|
@@ -113,11 +136,9 @@ class TriggerClient(TriggerClientLike):
|
|
|
113
136
|
async def _post(
|
|
114
137
|
self,
|
|
115
138
|
operation: TriggerClientOperation,
|
|
116
|
-
|
|
139
|
+
url: str,
|
|
117
140
|
body: TriggerChannelActionRequest | TriggerChannelRuntimeEventRequest,
|
|
118
141
|
) -> object:
|
|
119
|
-
encoded_channel_id = urllib.parse.quote(channel_id, safe="")
|
|
120
|
-
url = f"{self._api_base}/channels/{encoded_channel_id}/{operation}"
|
|
121
142
|
headers = {
|
|
122
143
|
"content-type": "application/json",
|
|
123
144
|
"x-api-key": self._api_key,
|
|
@@ -144,6 +165,13 @@ class TriggerClient(TriggerClientLike):
|
|
|
144
165
|
raise TriggerClientError(operation) from None
|
|
145
166
|
|
|
146
167
|
|
|
168
|
+
def _parse_action_response(response: object) -> TriggerChannelActionResponse:
|
|
169
|
+
response_id = response.get("id") if isinstance(response, dict) else None
|
|
170
|
+
if not isinstance(response_id, str) or not response_id.strip():
|
|
171
|
+
raise TriggerClientError("actions")
|
|
172
|
+
return {"id": response_id}
|
|
173
|
+
|
|
174
|
+
|
|
147
175
|
def create_trigger_client(
|
|
148
176
|
*,
|
|
149
177
|
user_id: str | None = None,
|
|
@@ -66,14 +66,17 @@ class TriggerChannelActionMessage(TypedDict):
|
|
|
66
66
|
text: str
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
class
|
|
69
|
+
class TriggerChannelActionRequestRequired(TypedDict):
|
|
70
70
|
type: Literal["post"]
|
|
71
71
|
action_id: str
|
|
72
72
|
address: TriggerChannelAddress
|
|
73
|
-
source_thread_key: str
|
|
74
73
|
message: TriggerChannelActionMessage
|
|
75
74
|
|
|
76
75
|
|
|
76
|
+
class TriggerChannelActionRequest(TriggerChannelActionRequestRequired, total=False):
|
|
77
|
+
source_thread_key: str
|
|
78
|
+
|
|
79
|
+
|
|
77
80
|
class TriggerChannelActionResponse(TypedDict):
|
|
78
81
|
id: str
|
|
79
82
|
|
|
@@ -9,7 +9,7 @@ name, before any managed ``{server}__`` prefix is applied.
|
|
|
9
9
|
Example::
|
|
10
10
|
|
|
11
11
|
# connectors/mcp.py
|
|
12
|
-
from managed_deepagents import connectors
|
|
12
|
+
from managed_deepagents import connectors
|
|
13
13
|
|
|
14
14
|
connector = connectors.mcp(
|
|
15
15
|
use_standard_content_blocks=True,
|
|
@@ -21,11 +21,6 @@ Example::
|
|
|
21
21
|
"url": "https://docs.example.com/mcp",
|
|
22
22
|
"include_tools": ["search", "fetch"],
|
|
23
23
|
},
|
|
24
|
-
"notion": {
|
|
25
|
-
"transport": "http",
|
|
26
|
-
"url": "https://mcp.notion.com/mcp",
|
|
27
|
-
"credential": credentials.get("notion", {"type": "user"}),
|
|
28
|
-
},
|
|
29
24
|
},
|
|
30
25
|
)
|
|
31
26
|
"""
|
|
@@ -47,7 +42,6 @@ from pydantic import (
|
|
|
47
42
|
|
|
48
43
|
from managed_deepagents._connector import Connector
|
|
49
44
|
from managed_deepagents._connectors.mcp.load import load_managed_mcp_tools
|
|
50
|
-
from managed_deepagents._credentials import CredentialReference # noqa: TC001
|
|
51
45
|
from managed_deepagents._extension import define_extension
|
|
52
46
|
|
|
53
47
|
|
|
@@ -59,9 +53,6 @@ class McpToolSelectionConfig(TypedDict, total=False):
|
|
|
59
53
|
class RemoteMcpServerBaseConfig(McpToolSelectionConfig, total=False):
|
|
60
54
|
url: str
|
|
61
55
|
headers: dict[str, str]
|
|
62
|
-
#: Bearer credential: an agent owner is a deployment-wide secret read once
|
|
63
|
-
#: at load, a user owner is the caller's OAuth grant loaded after the gate.
|
|
64
|
-
credential: CredentialReference
|
|
65
56
|
#: Default timeout in seconds for tools loaded from this server.
|
|
66
57
|
default_tool_timeout: float
|
|
67
58
|
|
|
@@ -151,7 +142,7 @@ class _RemoteMcpServer(BaseModel):
|
|
|
151
142
|
``default_tool_timeout``) are stripped before the client is constructed.
|
|
152
143
|
"""
|
|
153
144
|
|
|
154
|
-
model_config = ConfigDict(extra="allow", validate_default=True
|
|
145
|
+
model_config = ConfigDict(extra="allow", validate_default=True)
|
|
155
146
|
|
|
156
147
|
# Both default to a value the validators below reject, so a missing field
|
|
157
148
|
# reports the same message a malformed one does — as it does in Zod.
|
|
@@ -163,7 +154,6 @@ class _RemoteMcpServer(BaseModel):
|
|
|
163
154
|
default_tool_timeout: float | None = None
|
|
164
155
|
include_tools: list[_ToolName] | None = None
|
|
165
156
|
exclude_tools: list[_ToolName] | None = None
|
|
166
|
-
credential: CredentialReference | None = None
|
|
167
157
|
|
|
168
158
|
@field_validator("default_tool_timeout")
|
|
169
159
|
@classmethod
|
|
@@ -10,13 +10,10 @@ from __future__ import annotations
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import math
|
|
12
12
|
import warnings
|
|
13
|
-
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
|
14
|
-
from functools import partial
|
|
13
|
+
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
|
15
14
|
from typing import TYPE_CHECKING, Protocol, cast
|
|
16
15
|
|
|
17
16
|
from managed_deepagents._connectors.tool_selection import select_tools
|
|
18
|
-
from managed_deepagents._credential_gate import CredentialConsumer
|
|
19
|
-
from managed_deepagents._credentials import CredentialReference, credentials
|
|
20
17
|
from managed_deepagents._memory.async_util import await_if_needed
|
|
21
18
|
from managed_deepagents._memory.deps import (
|
|
22
19
|
optional_runtime_symbols,
|
|
@@ -53,10 +50,6 @@ class _TimedTool(Protocol):
|
|
|
53
50
|
#: tools loaded once per declaration, and every subsequent run reuses them.
|
|
54
51
|
#: Multiple ``connectors.mcp(...)`` modules each get their own entry (keyed by
|
|
55
52
|
#: object identity).
|
|
56
|
-
#:
|
|
57
|
-
#: An agent-owned credential is read into this cache once, so rotating that
|
|
58
|
-
#: secret takes a redeploy. Servers with a user-owned credential are omitted
|
|
59
|
-
#: entirely; the credential gate loads those per caller.
|
|
60
53
|
_managed_mcp_tools: dict[int, list[object]] = {}
|
|
61
54
|
|
|
62
55
|
|
|
@@ -80,83 +73,6 @@ def load_managed_mcp_tools(connectors: McpServersDefinition) -> list[object]:
|
|
|
80
73
|
return tools
|
|
81
74
|
|
|
82
75
|
|
|
83
|
-
def mcp_credential_consumers(
|
|
84
|
-
connectors: Sequence[object] | None,
|
|
85
|
-
) -> list[CredentialConsumer]:
|
|
86
|
-
"""The credential gate's view of the declared MCP connectors.
|
|
87
|
-
|
|
88
|
-
One consumer per declaration that names a user-owned credential; a
|
|
89
|
-
declaration that needs no grant never reaches the gate.
|
|
90
|
-
"""
|
|
91
|
-
out: list[CredentialConsumer] = []
|
|
92
|
-
for connector in connectors or []:
|
|
93
|
-
if getattr(connector, "kind", None) != "mcp_servers":
|
|
94
|
-
continue
|
|
95
|
-
definition = cast("McpServersDefinition", connector)
|
|
96
|
-
references = _user_credential_references(definition)
|
|
97
|
-
if not references:
|
|
98
|
-
continue
|
|
99
|
-
out.append(
|
|
100
|
-
CredentialConsumer(
|
|
101
|
-
declaration=definition,
|
|
102
|
-
credentials=references,
|
|
103
|
-
load=partial(load_user_credential_mcp_tools, definition),
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
-
return out
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _user_credential_references(
|
|
110
|
-
connectors: McpServersDefinition,
|
|
111
|
-
) -> tuple[CredentialReference, ...]:
|
|
112
|
-
"""The declared references themselves: the gate checks their owner."""
|
|
113
|
-
config = getattr(connectors, "config", None)
|
|
114
|
-
servers = config.get("mcp_servers") if isinstance(config, Mapping) else None
|
|
115
|
-
if not isinstance(servers, Mapping):
|
|
116
|
-
return ()
|
|
117
|
-
by_slug: dict[str, CredentialReference] = {}
|
|
118
|
-
for server_config in servers.values():
|
|
119
|
-
if _uses_user_credential(server_config):
|
|
120
|
-
credential = cast("CredentialReference", server_config["credential"])
|
|
121
|
-
by_slug.setdefault(credential.slug, credential)
|
|
122
|
-
return tuple(by_slug.values())
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def load_user_credential_mcp_tools(
|
|
126
|
-
connectors: McpServersDefinition,
|
|
127
|
-
access_tokens: Mapping[str, str],
|
|
128
|
-
) -> list[object]:
|
|
129
|
-
"""Load tools for user-credential servers, once the gate confirms grants.
|
|
130
|
-
|
|
131
|
-
Not process-cached here: the Authorization header is per caller, so the gate
|
|
132
|
-
owns the cache. Python's ``MultiServerMCPClient`` opens a session per call
|
|
133
|
-
and holds no transport, so there is nothing to close afterwards.
|
|
134
|
-
"""
|
|
135
|
-
config = connectors.config
|
|
136
|
-
servers = config["mcp_servers"]
|
|
137
|
-
_eager, user_credential = _partition_mcp_servers(servers)
|
|
138
|
-
if not user_credential:
|
|
139
|
-
return []
|
|
140
|
-
|
|
141
|
-
connectable: dict[str, RemoteMcpServerConfig] = {}
|
|
142
|
-
for server_name in user_credential:
|
|
143
|
-
server_config = servers.get(server_name)
|
|
144
|
-
if not isinstance(server_config, dict):
|
|
145
|
-
continue
|
|
146
|
-
credential = server_config.get("credential")
|
|
147
|
-
if not isinstance(credential, CredentialReference):
|
|
148
|
-
continue
|
|
149
|
-
access_token = access_tokens.get(credential.slug)
|
|
150
|
-
if not access_token:
|
|
151
|
-
raise RuntimeError(f'MCP user credential "{credential.slug}" has no access token')
|
|
152
|
-
connectable[server_name] = _with_bearer(server_config, access_token)
|
|
153
|
-
|
|
154
|
-
if not connectable:
|
|
155
|
-
return []
|
|
156
|
-
|
|
157
|
-
return _load_tools_from_servers(connectors, connectable)
|
|
158
|
-
|
|
159
|
-
|
|
160
76
|
def clear_managed_mcp_tools_cache_for_tests(
|
|
161
77
|
connectors: McpServersDefinition | None = None,
|
|
162
78
|
) -> None:
|
|
@@ -167,92 +83,14 @@ def clear_managed_mcp_tools_cache_for_tests(
|
|
|
167
83
|
_managed_mcp_tools.clear()
|
|
168
84
|
|
|
169
85
|
|
|
170
|
-
def _uses_user_credential(server_config: RemoteMcpServerConfig | dict[str, object]) -> bool:
|
|
171
|
-
if isinstance(server_config, dict):
|
|
172
|
-
credential = server_config.get("credential")
|
|
173
|
-
else:
|
|
174
|
-
credential = getattr(server_config, "credential", None)
|
|
175
|
-
return isinstance(credential, CredentialReference) and credential.owner["type"] == "user"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _partition_mcp_servers(
|
|
179
|
-
servers: dict[str, RemoteMcpServerConfig],
|
|
180
|
-
) -> tuple[dict[str, RemoteMcpServerConfig], list[str]]:
|
|
181
|
-
eager: dict[str, RemoteMcpServerConfig] = {}
|
|
182
|
-
user_credential: list[str] = []
|
|
183
|
-
for server_name, server_config in servers.items():
|
|
184
|
-
if _uses_user_credential(server_config):
|
|
185
|
-
user_credential.append(server_name)
|
|
186
|
-
else:
|
|
187
|
-
eager[server_name] = server_config
|
|
188
|
-
return eager, user_credential
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def _warn_deferred_user_credential_servers(server_names: list[str]) -> None:
|
|
192
|
-
if not server_names:
|
|
193
|
-
return
|
|
194
|
-
warnings.warn(
|
|
195
|
-
f"MCP server(s) {', '.join(server_names)} declare a user credential; "
|
|
196
|
-
"tools load after the pre-run credential gate confirms grants for the caller.",
|
|
197
|
-
RuntimeWarning,
|
|
198
|
-
stacklevel=3,
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
86
|
def _create_client_and_load_tools(connectors: McpServersDefinition) -> list[object]:
|
|
203
|
-
"""Create the ``langchain-mcp-adapters`` client and load its
|
|
87
|
+
"""Create the ``langchain-mcp-adapters`` client and load its tools.
|
|
204
88
|
|
|
205
89
|
Python's ``MultiServerMCPClient`` does not prefix tool names itself, so when
|
|
206
90
|
``prefix_tool_name_with_server_name`` is set (the default) MDA loads each
|
|
207
91
|
server's tools and namespaces them as ``{server}__{tool}``, matching the
|
|
208
92
|
TypeScript adapter's format.
|
|
209
|
-
|
|
210
|
-
Servers with user credential references are omitted; the gate loads them
|
|
211
|
-
per caller after OAuth grants exist.
|
|
212
93
|
"""
|
|
213
|
-
config = connectors.config
|
|
214
|
-
servers = config["mcp_servers"]
|
|
215
|
-
eager, user_credential = _partition_mcp_servers(servers)
|
|
216
|
-
if user_credential:
|
|
217
|
-
_warn_deferred_user_credential_servers(user_credential)
|
|
218
|
-
if not eager:
|
|
219
|
-
return []
|
|
220
|
-
return _load_tools_from_servers(connectors, _with_agent_credentials(eager))
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def _with_agent_credentials(
|
|
224
|
-
servers: dict[str, RemoteMcpServerConfig],
|
|
225
|
-
) -> dict[str, RemoteMcpServerConfig]:
|
|
226
|
-
"""Resolve each agent-owned credential into a static bearer header."""
|
|
227
|
-
out: dict[str, RemoteMcpServerConfig] = {}
|
|
228
|
-
for server_name, server_config in servers.items():
|
|
229
|
-
credential = server_config.get("credential") if isinstance(server_config, dict) else None
|
|
230
|
-
if not isinstance(credential, CredentialReference):
|
|
231
|
-
out[server_name] = server_config
|
|
232
|
-
continue
|
|
233
|
-
secret = await_if_needed(credentials.resolve_deployment_secret(credential.slug))
|
|
234
|
-
out[server_name] = _with_bearer(server_config, cast("str", secret))
|
|
235
|
-
return out
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _with_bearer(
|
|
239
|
-
server_config: RemoteMcpServerConfig,
|
|
240
|
-
token: str,
|
|
241
|
-
) -> RemoteMcpServerConfig:
|
|
242
|
-
merged = dict(cast("Mapping[str, object]", server_config))
|
|
243
|
-
raw_headers = merged.get("headers")
|
|
244
|
-
headers: dict[str, str] = (
|
|
245
|
-
dict(cast("Mapping[str, str]", raw_headers)) if isinstance(raw_headers, Mapping) else {}
|
|
246
|
-
)
|
|
247
|
-
headers["Authorization"] = f"Bearer {token}"
|
|
248
|
-
merged["headers"] = headers
|
|
249
|
-
return cast("RemoteMcpServerConfig", merged)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _load_tools_from_servers(
|
|
253
|
-
connectors: McpServersDefinition,
|
|
254
|
-
connectable: dict[str, RemoteMcpServerConfig],
|
|
255
|
-
) -> list[object]:
|
|
256
94
|
client_cls = cast(
|
|
257
95
|
"type",
|
|
258
96
|
require_runtime_dependency(
|
|
@@ -263,13 +101,14 @@ def _load_tools_from_servers(
|
|
|
263
101
|
)
|
|
264
102
|
|
|
265
103
|
config = connectors.config
|
|
104
|
+
servers = config["mcp_servers"]
|
|
266
105
|
prefix = config.get("prefix_tool_name_with_server_name", True)
|
|
267
106
|
throw_on_load_error = config.get("throw_on_load_error", True)
|
|
268
107
|
|
|
269
|
-
client = client_cls(_strip_managed_tool_selection(
|
|
108
|
+
client = client_cls(_strip_managed_tool_selection(servers))
|
|
270
109
|
|
|
271
110
|
tools: list[object] = []
|
|
272
|
-
for server_name, server_config in
|
|
111
|
+
for server_name, server_config in servers.items():
|
|
273
112
|
try:
|
|
274
113
|
server_tools = await_if_needed(client.get_tools(server_name=server_name))
|
|
275
114
|
except Exception as exc:
|
|
@@ -304,12 +143,7 @@ def _load_tools_from_servers(
|
|
|
304
143
|
|
|
305
144
|
#: Fields MDA owns. ``langchain-mcp-adapters`` unpacks the remaining dict into
|
|
306
145
|
#: the transport session constructor, which does not accept these keys.
|
|
307
|
-
_MDA_OWNED_SERVER_KEYS = (
|
|
308
|
-
"include_tools",
|
|
309
|
-
"exclude_tools",
|
|
310
|
-
"default_tool_timeout",
|
|
311
|
-
"credential",
|
|
312
|
-
)
|
|
146
|
+
_MDA_OWNED_SERVER_KEYS = ("include_tools", "exclude_tools", "default_tool_timeout")
|
|
313
147
|
|
|
314
148
|
|
|
315
149
|
def _strip_managed_tool_selection(
|
|
@@ -274,7 +274,7 @@ async def _authenticate(
|
|
|
274
274
|
async def resolve_request_identity(
|
|
275
275
|
cfg: IdentityConfig, request: HttpRequestLike
|
|
276
276
|
) -> RuntimeIdentity:
|
|
277
|
-
"""Resolve the caller's frozen identity
|
|
277
|
+
"""Resolve the caller's frozen ``runtime.identity`` from a raw HTTP request.
|
|
278
278
|
|
|
279
279
|
Uses the same ingress logic (``trusted_backend`` headers / ``validated_token``
|
|
280
280
|
verification) as the managed auth handler, then builds the frozen identity
|