managed-deepagents 0.6.2.dev53__py3-none-win_amd64.whl → 0.6.2.dev55__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.
@@ -8,7 +8,7 @@ This package provides two things:
8
8
  ``mda`` console script.
9
9
 
10
10
  Resources use ``define_*`` factories while provider collections use namespaces:
11
- ``define_sandbox()``, ``channels.slack(...)``, ``connectors.mcp(...)``,
11
+ ``define_sandbox()``, ``define_mcp()``, ``channels.slack(...)``,
12
12
  ``connections.get(...)``, and ``auth.supabase(...)``. Contributing a *new*
13
13
  primitive is not part of the public surface.
14
14
  """
@@ -32,6 +32,7 @@ from managed_deepagents.namespaces import (
32
32
  auth,
33
33
  channels,
34
34
  connectors,
35
+ define_mcp,
35
36
  define_sandbox,
36
37
  )
37
38
  from managed_deepagents.sandbox import SandboxDefinition
@@ -53,6 +54,7 @@ __all__ = [
53
54
  "connectors",
54
55
  "define_deep_agent",
55
56
  "define_identity",
57
+ "define_mcp",
56
58
  "define_memory",
57
59
  "define_sandbox",
58
60
  "define_schedule",
@@ -95,8 +95,8 @@ class WorkspaceConnection:
95
95
  class CredentialAuthorizationRequiredError(RuntimeError):
96
96
  def __init__(self, slugs: list[str]) -> None:
97
97
  super().__init__(
98
- f"the caller has not connected: {', '.join(slugs)}. Declare the connection "
99
- "on a connector so the pre-run gate can request it."
98
+ f"the caller has not connected: {', '.join(slugs)}. "
99
+ "The authorization interrupt resumed without a usable credential."
100
100
  )
101
101
  self.slugs = slugs
102
102
 
@@ -309,9 +309,14 @@ class AgentAuth:
309
309
  return None
310
310
  return self.read_secret(credential_id)
311
311
 
312
- def read_connection_access_token(self, connection_id: str) -> str | None:
313
- """Read an OAuth access token stored on a workspace connection row."""
314
- return self._fetch_access_token(connection_id)
312
+ def read_agent_access_token_for_connection(self, connection: WorkspaceConnection) -> str | None:
313
+ """Read this deployment's OAuth grant for a workspace connection."""
314
+ agent_id = (self.context.agent_id or "").strip()
315
+ if not agent_id:
316
+ raise CredentialStoreError(
317
+ "connections are not configured: LANGSMITH_HOST_PROJECT_ID is not set"
318
+ )
319
+ return self._read_access_token_for_owner(connection, "agent", agent_id)
315
320
 
316
321
  def read_user_access_token(self, slug: str) -> str | None:
317
322
  """Read the caller's OAuth access token without starting a session.
@@ -330,10 +335,18 @@ class AgentAuth:
330
335
 
331
336
  def read_user_access_token_for_connection(self, connection: WorkspaceConnection) -> str | None:
332
337
  """Read a caller OAuth token when the connection row is already loaded."""
338
+ return self._read_access_token_for_owner(connection, "user", self.context.principal_id)
339
+
340
+ def _read_access_token_for_owner(
341
+ self,
342
+ connection: WorkspaceConnection,
343
+ owner_type: str,
344
+ owner_id: str,
345
+ ) -> str | None:
333
346
  if connection.kind != "oauth2":
334
347
  return None
335
348
  credential_id = self._credential_id_for_owner(
336
- connection.connection_id, "user", self.context.principal_id
349
+ connection.connection_id, owner_type, owner_id
337
350
  )
338
351
  if credential_id is None:
339
352
  return None
Binary file
@@ -0,0 +1,96 @@
1
+ """Resolve user-owned connections or interrupt for authorization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from dataclasses import dataclass
7
+ from importlib import import_module
8
+ from typing import Literal, TypedDict, cast
9
+
10
+ from managed_deepagents._agent_auth import (
11
+ AgentAuth,
12
+ CredentialAuthorizationRequiredError,
13
+ )
14
+ from managed_deepagents._agent_auth_http import CredentialStoreError
15
+
16
+
17
+ class PendingCredential(TypedDict, total=False):
18
+ slug: str
19
+ kind: Literal["oauth2", "secret"]
20
+ connect_url: str
21
+ auth_id: str
22
+
23
+
24
+ class CredentialAuthorizationRequiredInterrupt(TypedDict):
25
+ """Interrupt payload when the caller has not granted one or more connections."""
26
+
27
+ type: str
28
+ message: str
29
+ credentials: list[PendingCredential]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class _UserConnectionGrant:
34
+ slug: str
35
+ kind: Literal["oauth2", "secret"]
36
+ token: str | None
37
+
38
+
39
+ InterruptFn = Callable[[CredentialAuthorizationRequiredInterrupt], None]
40
+
41
+
42
+ def resolve_user_connection_grants(
43
+ agent_auth: AgentAuth,
44
+ slugs: Sequence[str],
45
+ *,
46
+ interrupt: InterruptFn | None = None,
47
+ ) -> dict[str, str]:
48
+ """Return user connection material or interrupt once for every missing grant."""
49
+ grants = [_read_user_connection_grant(agent_auth, slug) for slug in dict.fromkeys(slugs)]
50
+ missing_oauth = [grant for grant in grants if grant.token is None and grant.kind == "oauth2"]
51
+ missing_secret = [grant for grant in grants if grant.token is None and grant.kind == "secret"]
52
+ if missing_oauth or missing_secret:
53
+ sessions = [agent_auth.start_authorization_session(grant.slug) for grant in missing_oauth]
54
+ payload: CredentialAuthorizationRequiredInterrupt = {
55
+ "type": "credential_authorization_required",
56
+ "message": "Connect the following integrations to continue.",
57
+ "credentials": [
58
+ {
59
+ "slug": session["slug"],
60
+ "kind": "oauth2",
61
+ "connect_url": session["connect_url"],
62
+ "auth_id": session["auth_id"],
63
+ }
64
+ for session in sessions
65
+ ]
66
+ + [{"slug": grant.slug, "kind": "secret"} for grant in missing_secret],
67
+ }
68
+ (interrupt or _interrupt)(payload)
69
+ raise CredentialAuthorizationRequiredError(
70
+ [grant.slug for grant in missing_oauth] + [grant.slug for grant in missing_secret]
71
+ )
72
+ return {grant.slug: cast("str", grant.token) for grant in grants}
73
+
74
+
75
+ def _read_user_connection_grant(agent_auth: AgentAuth, slug: str) -> _UserConnectionGrant:
76
+ row = agent_auth.find_workspace_connection(slug)
77
+ if row is None:
78
+ raise CredentialStoreError(f"workspace connection {slug!r} is not configured")
79
+ kind: Literal["oauth2", "secret"] = "secret" if row.kind == "secret" else "oauth2"
80
+ token = (
81
+ agent_auth.read_user_secret_for_connection(row)
82
+ if kind == "secret"
83
+ else agent_auth.read_user_access_token_for_connection(row)
84
+ )
85
+ return _UserConnectionGrant(slug=slug, kind=kind, token=token)
86
+
87
+
88
+ def _interrupt(payload: CredentialAuthorizationRequiredInterrupt) -> None:
89
+ import_module("langgraph.types").interrupt(payload)
90
+
91
+
92
+ __all__ = [
93
+ "CredentialAuthorizationRequiredInterrupt",
94
+ "PendingCredential",
95
+ "resolve_user_connection_grants",
96
+ ]
@@ -6,7 +6,9 @@ reads Agent Auth on every await and never caches material.
6
6
 
7
7
  ``mda dev`` substitutes local values for agent opaque secrets via ``MDA_DEV_*``.
8
8
  User-owned connections talk to Agent Auth in every environment (including
9
- ``mda dev``) after resolving the caller's Agent Auth principal.
9
+ ``mda dev``) after resolving the caller's Agent Auth principal. Missing grants
10
+ interrupt at the point of access, including calls from custom tools and
11
+ middleware.
10
12
  """
11
13
 
12
14
  from __future__ import annotations
@@ -253,25 +255,18 @@ def _lookup_workspace_connection(
253
255
  slug: str,
254
256
  owner: ConnectionOwner,
255
257
  ) -> str | None:
258
+ if owner["type"] == "user":
259
+ from managed_deepagents._connection_authorization import ( # noqa: PLC0415
260
+ resolve_user_connection_grants,
261
+ )
262
+
263
+ return resolve_user_connection_grants(client, [slug])[slug]
256
264
  row = client.find_workspace_connection(slug)
257
265
  if row is None:
258
266
  return None
259
- if owner["type"] == "user":
260
- return _lookup_user_connection_material(client, row)
261
267
  return _lookup_agent_connection_material(client, slug, row)
262
268
 
263
269
 
264
- def _lookup_user_connection_material(
265
- client: AgentAuth,
266
- row: WorkspaceConnection,
267
- ) -> str | None:
268
- if row.kind == "secret":
269
- return client.read_user_secret_for_connection(row)
270
- if row.kind == "oauth2":
271
- return client.read_user_access_token_for_connection(row)
272
- return None
273
-
274
-
275
270
  def _lookup_agent_connection_material(
276
271
  client: AgentAuth,
277
272
  slug: str,
@@ -283,7 +278,7 @@ def _lookup_agent_connection_material(
283
278
  return None
284
279
  return client.read_secret(credential_id)
285
280
  if row.kind == "oauth2":
286
- return client.read_connection_access_token(row.connection_id)
281
+ return client.read_agent_access_token_for_connection(row)
287
282
  return None
288
283
 
289
284
 
@@ -4,9 +4,8 @@ First-party connectors each live in their own subpackage:
4
4
 
5
5
  - :mod:`managed_deepagents._connectors.mcp` — MCP tools
6
6
 
7
- A project selects them through the ``connectors`` namespace
8
- (``connectors.mcp(...)``); the compiler discovers the result by shape (a
9
- module-level ``connector`` attribute), not by filename.
7
+ A project selects them with ``define_mcp(...)``; the compiler discovers the
8
+ result by shape (a module-level ``connector`` attribute), not by filename.
10
9
  """
11
10
 
12
11
  from __future__ import annotations
@@ -1,6 +1,6 @@
1
- """MCP connector authoring surface for the Python ecosystem.
1
+ """MCP authoring surface for the Python ecosystem.
2
2
 
3
- ``connectors.mcp`` implements the ``tools`` hook. Only remote MCP servers over
3
+ ``define_mcp`` implements the ``tools`` hook. Only remote MCP servers over
4
4
  HTTP/SSE are supported; stdio is intentionally out of scope. By default, all
5
5
  tools loaded from each declared server are exposed. Use ``include_tools`` or
6
6
  ``exclude_tools`` inside a server config to select a subset by raw MCP tool
@@ -8,14 +8,14 @@ name, before any managed ``{server}__`` prefix is applied.
8
8
 
9
9
  Example::
10
10
 
11
- # connectors/mcp.py
12
- from managed_deepagents import connectors, connections
11
+ # tools/mcp.py
12
+ from managed_deepagents import define_mcp, connections
13
13
 
14
- connector = connectors.mcp(
14
+ mcp = define_mcp(
15
15
  use_standard_content_blocks=True,
16
16
  prefix_tool_name_with_server_name=True,
17
17
  throw_on_load_error=True,
18
- mcp_servers={
18
+ servers={
19
19
  "docs": {
20
20
  "transport": "http",
21
21
  "url": "https://docs.example.com/mcp",
@@ -89,25 +89,33 @@ class SseMcpServerConfig(RemoteMcpServerBaseConfig, total=False):
89
89
  RemoteMcpServerConfig = HttpMcpServerConfig | SseMcpServerConfig
90
90
 
91
91
 
92
- class McpServersConfigRequired(TypedDict):
93
- mcp_servers: dict[str, RemoteMcpServerConfig]
94
-
95
-
96
- class McpServersConfig(McpServersConfigRequired, total=False):
92
+ class McpServersConfigOptions(TypedDict, total=False):
97
93
  prefix_tool_name_with_server_name: bool
98
94
  throw_on_load_error: bool
99
95
  use_standard_content_blocks: bool
100
96
  on_connection_error: Literal["throw"]
101
97
 
102
98
 
103
- __all__ = ["McpServersConfig", "McpServersDefinition", "mcp_connector"]
99
+ class McpServersConfigRequired(McpServersConfigOptions):
100
+ servers: dict[str, RemoteMcpServerConfig]
101
+
102
+
103
+ class McpServersConfigLegacy(McpServersConfigOptions):
104
+ #: Deprecated alias of ``servers``. Removed in 0.8.0.
105
+ mcp_servers: dict[str, RemoteMcpServerConfig]
106
+
107
+
108
+ McpServersConfig = McpServersConfigRequired | McpServersConfigLegacy
109
+
110
+
111
+ __all__ = ["McpServersConfig", "McpServersDefinition", "define_mcp"]
104
112
 
105
113
  #: Remote transports MDA supports. Stdio is intentionally unsupported in v0.
106
114
  _REMOTE_TRANSPORTS = ("http", "sse")
107
115
 
108
116
 
109
117
  class McpServersDefinition(Connector):
110
- """The MCP connector returned by ``connectors.mcp(...)``.
118
+ """The MCP connector returned by ``define_mcp(...)``.
111
119
 
112
120
  A branded managed :class:`Connector` whose ``tools`` hook loads the declared
113
121
  MCP tools at runtime; MDA's generated entry hands it to the managed runtime,
@@ -117,7 +125,8 @@ class McpServersDefinition(Connector):
117
125
  kind = "mcp_servers"
118
126
 
119
127
  def __init__(self, **config: Any) -> None:
120
- self.config = cast("McpServersConfig", config)
128
+ # The argument model rewrites the legacy key before construction.
129
+ self.config = cast("McpServersConfigRequired", config)
121
130
 
122
131
  def tools(self, ctx: Mapping[str, object] | None = None) -> Sequence[object]: # noqa: ARG002
123
132
  """Load (and cache) the declared MCP tools."""
@@ -127,7 +136,7 @@ class McpServersDefinition(Connector):
127
136
  """Describe user and agent connections that deploy must validate."""
128
137
  user_slugs: set[str] = set()
129
138
  agent_slugs: set[str] = set()
130
- for server in self.config["mcp_servers"].values():
139
+ for server in self.config["servers"].values():
131
140
  connection = server.get("connection") if isinstance(server, dict) else None
132
141
  if not is_connection_reference(connection):
133
142
  continue
@@ -148,7 +157,7 @@ class McpServersDefinition(Connector):
148
157
  }
149
158
 
150
159
  def __repr__(self) -> str: # pragma: no cover - debug helper
151
- servers = ", ".join(sorted(self.config.get("mcp_servers", {})))
160
+ servers = ", ".join(sorted(self.config.get("servers", {})))
152
161
  return f"McpServersDefinition(servers=[{servers}])"
153
162
 
154
163
 
@@ -165,6 +174,12 @@ _CREDENTIAL_FIELD_DEPRECATION = (
165
174
 
166
175
  _CREDENTIAL_FIELD_CONFLICT = 'use either "connection" or "credential", not both'
167
176
 
177
+ _MCP_SERVERS_KEY_DEPRECATION = (
178
+ "mcp_servers is deprecated; use servers instead. It will be removed in 0.8.0."
179
+ )
180
+
181
+ _SERVERS_KEY_CONFLICT = 'use either "servers" or "mcp_servers", not both'
182
+
168
183
 
169
184
  def _non_empty_tool_name(name: str) -> str:
170
185
  if not name.strip():
@@ -275,13 +290,29 @@ class _McpServersArgs(BaseModel):
275
290
 
276
291
  model_config = ConfigDict(extra="forbid")
277
292
 
278
- mcp_servers: dict[str, _RemoteMcpServer]
293
+ servers: dict[str, _RemoteMcpServer]
279
294
  prefix_tool_name_with_server_name: StrictBool = True
280
295
  throw_on_load_error: StrictBool = True
281
296
  use_standard_content_blocks: StrictBool = True
282
297
  on_connection_error: str = "throw"
283
298
 
284
- @field_validator("mcp_servers")
299
+ @model_validator(mode="before")
300
+ @classmethod
301
+ def _normalize_servers_key(cls, data: Any) -> Any:
302
+ if not isinstance(data, Mapping):
303
+ return data
304
+ has_servers = "servers" in data
305
+ has_legacy = "mcp_servers" in data
306
+ if has_servers and has_legacy:
307
+ raise ValueError(_SERVERS_KEY_CONFLICT)
308
+ if has_legacy:
309
+ warnings.warn(_MCP_SERVERS_KEY_DEPRECATION, FutureWarning, stacklevel=2)
310
+ normalized = dict(data)
311
+ normalized["servers"] = normalized.pop("mcp_servers")
312
+ return normalized
313
+ return data
314
+
315
+ @field_validator("servers")
285
316
  @classmethod
286
317
  def _at_least_one_server(
287
318
  cls, servers: dict[str, _RemoteMcpServer]
@@ -308,9 +339,9 @@ class _McpServersArgs(BaseModel):
308
339
 
309
340
 
310
341
  #: Declare the MCP servers whose tools should be loaded into the agent.
311
- mcp_connector = define_extension(
342
+ define_mcp = define_extension(
312
343
  "connector",
313
- label="connectors.mcp",
344
+ label="define_mcp",
314
345
  connector=McpServersDefinition,
315
346
  args=_McpServersArgs,
316
347
  )
@@ -1,7 +1,7 @@
1
1
  """Managed connector loaders (MCP tool loading for the managed runtime).
2
2
 
3
3
  Mirrors the TypeScript ``connectors/mcp/load`` module. Kept separate from
4
- ``runtime.py`` so the authoring ``connectors`` package can delegate MCP loading
4
+ ``runtime.py`` so the authoring MCP factory can delegate loading
5
5
  without importing the full managed runtime seam.
6
6
  """
7
7
 
@@ -55,7 +55,7 @@ class _TimedTool(Protocol):
55
55
  #: every invocation, but each connector's MCP connection and tool surface are
56
56
  #: stable for the life of the deployment — so the client is created and its
57
57
  #: tools loaded once per declaration, and every subsequent run reuses them.
58
- #: Multiple ``connectors.mcp(...)`` modules each get their own entry (keyed by
58
+ #: Multiple ``define_mcp(...)`` modules each get their own entry (keyed by
59
59
  #: object identity).
60
60
  #:
61
61
  #: An agent-owned connection is read into this cache once, so rotating that
@@ -115,7 +115,7 @@ def _user_connection_references(
115
115
  ) -> tuple[ConnectionReference, ...]:
116
116
  """The declared references themselves: the gate checks their owner."""
117
117
  config = getattr(connectors, "config", None)
118
- servers = config.get("mcp_servers") if isinstance(config, Mapping) else None
118
+ servers = config.get("servers") if isinstance(config, Mapping) else None
119
119
  if not isinstance(servers, Mapping):
120
120
  return ()
121
121
  by_slug: dict[str, ConnectionReference] = {}
@@ -137,7 +137,7 @@ def load_user_connection_mcp_tools(
137
137
  and holds no transport, so there is nothing to close afterwards.
138
138
  """
139
139
  config = connectors.config
140
- servers = config["mcp_servers"]
140
+ servers = config["servers"]
141
141
  _eager, user_connection = _partition_mcp_servers(servers)
142
142
  if not user_connection:
143
143
  return []
@@ -218,7 +218,7 @@ def _create_client_and_load_tools(connectors: McpServersDefinition) -> list[obje
218
218
  per caller after grants exist.
219
219
  """
220
220
  config = connectors.config
221
- servers = config["mcp_servers"]
221
+ servers = config["servers"]
222
222
  eager, user_connection = _partition_mcp_servers(servers)
223
223
  if user_connection:
224
224
  _warn_deferred_user_connection_servers(user_connection)
@@ -1,6 +1,6 @@
1
1
  """Tool-name selection, shared by the connectors that load tools over MCP.
2
2
 
3
- ``connectors.mcp`` selects by raw tool name across arbitrary servers. A name in
3
+ ``define_mcp`` selects by raw tool name across arbitrary servers. A name in
4
4
  ``include_tools``/``exclude_tools`` that no loaded tool answers to is an error
5
5
  naming what *was* available, so a typo fails the deployment instead of silently
6
6
  dropping a tool the agent was meant to have.
@@ -14,14 +14,16 @@ import time
14
14
  from collections.abc import Awaitable, Callable, Mapping, Sequence
15
15
  from dataclasses import dataclass, field
16
16
  from importlib import import_module
17
- from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
17
+ from typing import TYPE_CHECKING, Any, cast
18
18
 
19
19
  from managed_deepagents._agent_auth import (
20
20
  AgentAuth,
21
21
  AgentAuthContext,
22
- CredentialAuthorizationRequiredError,
23
- PendingAuthorization,
24
- missing_workspace_connection_error,
22
+ )
23
+ from managed_deepagents._connection_authorization import (
24
+ CredentialAuthorizationRequiredInterrupt,
25
+ PendingCredential,
26
+ resolve_user_connection_grants,
25
27
  )
26
28
  from managed_deepagents._connections import (
27
29
  ConnectionReference,
@@ -43,21 +45,6 @@ CREDENTIAL_GATE_MIDDLEWARE_NAME = "mda-credential-gate"
43
45
  _TOOL_CACHE_TTL_SECONDS = 5 * 60
44
46
 
45
47
 
46
- class PendingCredential(TypedDict, total=False):
47
- slug: str
48
- kind: Literal["oauth2", "secret"]
49
- connect_url: str
50
- auth_id: str
51
-
52
-
53
- class CredentialAuthorizationRequiredInterrupt(TypedDict):
54
- """Interrupt payload when the caller has not granted one or more connections."""
55
-
56
- type: str
57
- message: str
58
- credentials: list[PendingCredential]
59
-
60
-
61
48
  @dataclass(frozen=True)
62
49
  class CredentialConsumer:
63
50
  """A declaration whose tools are unlocked by user-owned connections."""
@@ -74,13 +61,6 @@ class _CacheEntry:
74
61
  declaration: object = field(repr=False)
75
62
 
76
63
 
77
- @dataclass(frozen=True)
78
- class _UserConnectionGrant:
79
- slug: str
80
- kind: Literal["oauth2", "secret"]
81
- token: str | None
82
-
83
-
84
64
  _tool_cache: dict[tuple[int, str], _CacheEntry] = {}
85
65
  _tool_cache_lock = threading.Lock()
86
66
  _tool_load_locks: dict[tuple[int, str], threading.Lock] = {}
@@ -102,13 +82,13 @@ def create_credential_gate_middleware(
102
82
 
103
83
  def before_agent(self, _state: object, runtime: object | None = None) -> None:
104
84
  caller = _require_auth_context(runtime)
105
- payload = _pending_interrupt(consumers, caller)
106
- _gate(consumers, caller.context, payload)
85
+ _ensure_grants(consumers, caller)
86
+ _load_all(consumers, caller.context)
107
87
 
108
88
  async def abefore_agent(self, _state: object, runtime: object | None = None) -> None:
109
89
  caller = await asyncio.to_thread(_require_auth_context, runtime)
110
- payload = await asyncio.to_thread(_pending_interrupt, consumers, caller)
111
- _gate(consumers, caller.context, payload)
90
+ await asyncio.to_thread(_ensure_grants, consumers, caller)
91
+ await asyncio.to_thread(_load_all, consumers, caller.context)
112
92
 
113
93
  def wrap_model_call(self, request: Any, handler: Callable[..., Any]) -> Any:
114
94
  return handler(_with_extra_tools(request, _tools_for(consumers, request)))
@@ -131,71 +111,17 @@ def create_credential_gate_middleware(
131
111
  return _CredentialGateMiddleware()
132
112
 
133
113
 
134
- def _gate(
135
- consumers: Sequence[CredentialConsumer],
136
- context: AgentAuthContext,
137
- payload: CredentialAuthorizationRequiredInterrupt | None,
138
- ) -> None:
139
- if payload is not None:
140
- _interrupt(payload)
141
- slugs = [item["slug"] for item in payload["credentials"]]
142
- raise CredentialAuthorizationRequiredError(slugs)
143
- _load_all(consumers, context)
144
-
145
-
146
- def _pending_interrupt(
114
+ def _ensure_grants(
147
115
  consumers: Sequence[CredentialConsumer],
148
116
  caller: _Caller,
149
- ) -> CredentialAuthorizationRequiredInterrupt | None:
117
+ ) -> None:
150
118
  agent_auth = AgentAuth(caller.context)
151
- grants = _read_user_connection_grants(agent_auth, _consumer_credentials(consumers, caller))
152
- missing_oauth = [grant for grant in grants if grant.token is None and grant.kind == "oauth2"]
153
- missing_secret = [grant for grant in grants if grant.token is None and grant.kind == "secret"]
154
- if not missing_oauth and not missing_secret:
155
- return None
156
-
157
- pending_oauth = [agent_auth.start_authorization_session(grant.slug) for grant in missing_oauth]
158
- credentials: list[PendingCredential] = [
159
- _pending_oauth_entry(session) for session in pending_oauth
160
- ] + [_pending_secret_entry(grant.slug) for grant in missing_secret]
161
- return {
162
- "type": "credential_authorization_required",
163
- "message": "Connect the following integrations to continue.",
164
- "credentials": credentials,
165
- }
166
-
167
-
168
- def _pending_oauth_entry(session: PendingAuthorization) -> PendingCredential:
169
- return {
170
- "slug": session["slug"],
171
- "kind": "oauth2",
172
- "connect_url": session["connect_url"],
173
- "auth_id": session["auth_id"],
174
- }
175
-
176
-
177
- def _pending_secret_entry(slug: str) -> PendingCredential:
178
- return {"slug": slug, "kind": "secret"}
179
-
180
-
181
- def _read_user_connection_grants(
182
- agent_auth: AgentAuth,
183
- references: Sequence[ConnectionReference],
184
- ) -> list[_UserConnectionGrant]:
185
- grants: list[_UserConnectionGrant] = []
186
- for reference in references:
187
- slug = reference.slug
188
- row = agent_auth.find_workspace_connection(slug)
189
- if row is None:
190
- raise missing_workspace_connection_error(slug)
191
- kind: Literal["oauth2", "secret"] = "secret" if row.kind == "secret" else "oauth2"
192
- token = (
193
- agent_auth.read_user_secret_for_connection(row)
194
- if kind == "secret"
195
- else agent_auth.read_user_access_token_for_connection(row)
196
- )
197
- grants.append(_UserConnectionGrant(slug=slug, kind=kind, token=token))
198
- return grants
119
+ credentials = _consumer_credentials(consumers, caller)
120
+ resolve_user_connection_grants(
121
+ agent_auth,
122
+ [credential.slug for credential in credentials],
123
+ interrupt=_interrupt,
124
+ )
199
125
 
200
126
 
201
127
  def _consumer_credentials(
@@ -269,11 +195,11 @@ def _load_tools(
269
195
  context: AgentAuthContext,
270
196
  ) -> list[object]:
271
197
  agent_auth = AgentAuth(context)
272
- grants = _read_user_connection_grants(agent_auth, consumer.credentials)
273
- missing = [grant.slug for grant in grants if grant.token is None]
274
- if missing:
275
- raise CredentialAuthorizationRequiredError(missing)
276
- access_tokens = {grant.slug: cast("str", grant.token) for grant in grants}
198
+ access_tokens = resolve_user_connection_grants(
199
+ agent_auth,
200
+ [credential.slug for credential in consumer.credentials],
201
+ interrupt=_interrupt,
202
+ )
277
203
  return list(wrap_tools_with_managed_runtime(consumer.load(access_tokens)))
278
204
 
279
205
 
@@ -1,11 +1,11 @@
1
1
  """The contribution API behind the extensible MDA primitives.
2
2
 
3
- ``define_extension(kind, ...)`` returns the factory a connector namespace calls,
4
- such as ``connectors.mcp(...)``. Argument parsing, branding, and manifest
3
+ ``define_extension(kind, ...)`` returns the factory a first-party primitive
4
+ calls, such as ``define_mcp(...)``. Argument parsing, branding, and manifest
5
5
  stamping are shared.
6
6
 
7
7
  This module is deliberately absent from the package's public surface. Only the
8
- first-party namespaces in :mod:`managed_deepagents.namespaces` contribute
8
+ first-party factories in :mod:`managed_deepagents.namespaces` contribute
9
9
  connectors, so an agent project cannot mount its own adapter or HTTP route into
10
10
  the managed runtime. Opening it up to third-party packages is a later step.
11
11
 
@@ -4,7 +4,7 @@ Provider collections use ``<primitive>.<provider>(...)`` namespaces, while
4
4
  resources use ``define_*`` factories::
5
5
 
6
6
  channels.slack(name="Support Bot", description="Answers support questions")
7
- connectors.mcp(mcp_servers={...})
7
+ define_mcp(servers={...})
8
8
  define_sandbox(default_timeout=600)
9
9
  auth.supabase(project_ref="abc")
10
10
 
@@ -14,18 +14,24 @@ contribute its own — :mod:`managed_deepagents._extension` stays internal.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import warnings
18
+
17
19
  from pydantic import BaseModel, ConfigDict, field_validator
18
20
 
19
21
  from managed_deepagents._channels.trigger.channel import slack_channel
20
- from managed_deepagents._connectors.mcp import mcp_connector
22
+ from managed_deepagents._connectors.mcp import McpServersDefinition, define_mcp
21
23
  from managed_deepagents.identity import auth
22
24
  from managed_deepagents.sandbox import SandboxDefinition, normalize_sandbox_options
23
25
 
24
- __all__ = ["auth", "channels", "connectors", "define_sandbox"]
26
+ __all__ = ["auth", "channels", "connectors", "define_mcp", "define_sandbox"]
27
+
28
+ _CONNECTORS_MCP_DEPRECATION = (
29
+ "connectors.mcp(...) is deprecated; use define_mcp(...) instead. It will be removed in 0.8.0."
30
+ )
25
31
 
26
32
 
27
33
  class _Namespace:
28
- """A namespace whose providers are attributes, e.g. ``connectors.mcp``.
34
+ """A namespace whose providers are attributes, e.g. ``channels.slack``.
29
35
 
30
36
  Subclasses set the providers as class attributes wrapped in
31
37
  :func:`staticmethod`, matching how the TypeScript namespaces are plain
@@ -37,23 +43,27 @@ class _Namespace:
37
43
  return f"{type(self).__name__.lstrip('_').lower()}({providers})"
38
44
 
39
45
 
40
- class _Connectors(_Namespace):
41
- """Integrations declared under ``connectors/``.
46
+ class _Channels(_Namespace):
47
+ """Inbound messaging surfaces (Events ingress + outbound messaging).
42
48
 
43
- This release ships only remote MCP servers. Authors attach them with
44
- ``connectors.mcp(...)`` and supply their own auth (static headers, etc.).
49
+ Slack ingress through Trigger Server.
45
50
  """
46
51
 
47
- mcp = staticmethod(mcp_connector)
52
+ slack = staticmethod(slack_channel)
48
53
 
49
54
 
50
- class _Channels(_Namespace):
51
- """Inbound messaging surfaces (Events ingress + outbound messaging).
55
+ def _deprecated_mcp(**kwargs: object) -> McpServersDefinition:
56
+ warnings.warn(_CONNECTORS_MCP_DEPRECATION, FutureWarning, stacklevel=2)
57
+ return define_mcp(**kwargs)
52
58
 
53
- Slack ingress through Trigger Server.
59
+
60
+ class _Connectors(_Namespace):
61
+ """Deprecated MCP namespace. Use :func:`define_mcp` instead.
62
+
63
+ ``connectors.mcp`` will be removed in 0.8.0.
54
64
  """
55
65
 
56
- slack = staticmethod(slack_channel)
66
+ mcp = staticmethod(_deprecated_mcp)
57
67
 
58
68
 
59
69
  class _SandboxRegistryArgs(BaseModel):
@@ -122,5 +132,5 @@ def define_sandbox(
122
132
  return SandboxDefinition(normalized)
123
133
 
124
134
 
125
- connectors = _Connectors()
126
135
  channels = _Channels()
136
+ connectors = _Connectors()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: managed-deepagents
3
- Version: 0.6.2.dev53
3
+ Version: 0.6.2.dev55
4
4
  Summary: Managed Deep Agents — the define_deep_agent authoring interface plus the CLI that compiles and deploys a code-first Deep Agent repository to a managed LangGraph runtime.
5
5
  Author: LangChain
6
6
  License: MIT
@@ -178,11 +178,11 @@ my-agent/
178
178
  pyproject.toml
179
179
  .env # local deploy secrets, never committed
180
180
  schedules/ # optional managed cron schedules
181
- tools/ # optional custom tools
181
+ tools/ # optional custom tools and MCP declaration
182
+ mcp.py # optional MCP server declaration
182
183
  middleware/ # optional middleware
183
184
  skills/ # optional skills synced to Context Hub
184
185
  sandbox/ # LangSmith sandbox (`mda init` includes this; delete to opt out)
185
- connectors/mcp.py # optional MCP server declaration
186
186
  ```
187
187
 
188
188
  The CLI copies your project files into the managed build and generates the entry
@@ -245,19 +245,22 @@ sandbox = define_sandbox(
245
245
  Put `GHCR_TOKEN` in the project `.env` or process environment. Its value is
246
246
  used only to reconcile the registry and never enters the build or snapshot.
247
247
 
248
- ## MCP Connectors
248
+ ## MCP Servers
249
249
 
250
- Add `connectors/mcp.py` to attach MCP servers. The file must define a
251
- module-level `connector`. By default, MDA exposes every tool loaded from each
250
+ Add `tools/mcp.py` to attach MCP servers. The file must define a module-level
251
+ `mcp`. By default, MDA exposes every tool loaded from each
252
252
  declared server. In `mda dev`, an agent-owned connection reads from
253
253
  `MDA_DEV_<SLUG>` with the slug uppercased and hyphens changed to underscores.
254
254
  Hosted deployments resolve workspace connections from Agent Auth:
255
255
 
256
+ The old `connectors/mcp.py` file API remains available with a warning during
257
+ `0.7.x`. It will be removed in `0.8.0`.
258
+
256
259
  ```python
257
- from managed_deepagents import connectors, connections
260
+ from managed_deepagents import define_mcp, connections
258
261
 
259
- connector = connectors.mcp(
260
- mcp_servers={
262
+ mcp = define_mcp(
263
+ servers={
261
264
  "langchainDocs": {
262
265
  "transport": "http",
263
266
  "url": "https://docs.langchain.com/mcp",
@@ -271,14 +274,15 @@ connector = connectors.mcp(
271
274
  For this example, set `MDA_DEV_DOCS_TOKEN`.
272
275
 
273
276
  A user-owned connection — `connections.get(slug, {"type": "user"})` — resolves
274
- per caller (OAuth or opaque). The runtime maps the verified ingress identity to
275
- an Agent Auth principal, then the pre-run credential gate interrupts the run
276
- when a grant is missing. That path works in `mda deploy` and `mda dev` when
277
- `LANGSMITH_API_KEY` and `LANGSMITH_WORKSPACE_ID` are set and the workspace
278
- connection rows exist — use a person principal (for example a Supabase JWT),
279
- not Studio’s synthetic service user. OAuth completes on LangSmith’s platform
280
- callback URL; the local UI must surface `credential_authorization_required`
281
- and resume after the user connects.
277
+ per caller (OAuth or opaque). Any runtime access interrupts the run when a grant
278
+ is missing, including access from a custom tool or middleware. Connector
279
+ declarations use the same behavior in a pre-run gate, so all known grants can be
280
+ requested before the first model call. This path works in `mda deploy` and
281
+ `mda dev` when `LANGSMITH_API_KEY` and `LANGSMITH_WORKSPACE_ID` are set and
282
+ the workspace connection rows exist. Use a person principal (for example, a
283
+ Supabase JWT), not Studio’s synthetic service user. OAuth completes on
284
+ LangSmith’s platform callback URL. The local UI must show
285
+ `credential_authorization_required` and resume after the user connects.
282
286
 
283
287
  Deploy the project first, then provision its connections with
284
288
  `mda connections create`. Agent-owned opaque secrets are scoped to that
@@ -1,15 +1,16 @@
1
- managed_deepagents/__init__.py,sha256=RezkpgYxCcywj6_W7Nk3PN9TMBfYyFgIatxGrIeY8V4,1862
1
+ managed_deepagents/__init__.py,sha256=KLkxKBS4uCcJKJr9CGo5GrVjeHHEjF8Wo6aSdWU4KJk,1889
2
2
  managed_deepagents/__main__.py,sha256=GcjmqN0Ea5v8OHKMxYToasm3eCX_doSLOlpEzAi1tAw,67
3
- managed_deepagents/_agent_auth.py,sha256=8QiCiGxYjbk3BTrOeCGVkQTLh6IzTUia18-_Mry9NWI,20018
3
+ managed_deepagents/_agent_auth.py,sha256=SkpbTWNV4jiwbhGGv230l1UTe2PHPbaSswTWqzGxvq0,20524
4
4
  managed_deepagents/_agent_auth_http.py,sha256=d-Fg62m-_5qBXj-d9xgfA3KGiPRfs7T2g6Q2nrhCetc,6065
5
5
  managed_deepagents/_channel.py,sha256=-Og7QOM1yvV-yeTDOfv2CxXpR2HLY4xdX1bLrIsPi5k,5272
6
6
  managed_deepagents/_cli.py,sha256=jVaw2i1ysdRoWziMdPFmRXtKry_baYGRspyzyIeGWoU,1688
7
- managed_deepagents/_connections.py,sha256=B0mrBbh0dhFxFMkdkYVAM5Q5WaKUcYJryizZh_Hxg3A,19963
7
+ managed_deepagents/_connection_authorization.py,sha256=WCuJ3-58lQIre-bcXspVx28Gzyg8m9F7KUMunk6CtVo,3387
8
+ managed_deepagents/_connections.py,sha256=oJmpqH_keBhc0VaY2mlc5EZlMZv8jDW0eoEcef0o5Bc,19896
8
9
  managed_deepagents/_connector.py,sha256=Syp0SRs1oFCUUltIUmZbSNi1Ys_PxObVdiAi-I9GBzs,8040
9
10
  managed_deepagents/_cors.py,sha256=V96RPrqewdRWG5RpEMEBnPuRq9_zF8nf4vf99k5dkuI,7597
10
- managed_deepagents/_credential_gate.py,sha256=VLjSc7k1MoZdMI8juZpdeOK7lYyvbb9OsFZGAhoj6t4,12287
11
+ managed_deepagents/_credential_gate.py,sha256=oeJSmdLoxwzAg28ALqUm4sWeV0sx__9iEKdNIYdccME,9539
11
12
  managed_deepagents/_eval_filesystem.py,sha256=1nuD0yn3gh19JrWNocWyPFyeJW42E7Gse3XRWCUSTAA,1399
12
- managed_deepagents/_extension.py,sha256=x2H1S8inkvTZQ_dr7kq0M8o8rre1FjzCDvMzItw4iyk,5318
13
+ managed_deepagents/_extension.py,sha256=MqCWS4AGxiURPtLlA2y_tWKdRL8Zu1-VZyQ6Q3HDgzc,5315
13
14
  managed_deepagents/_identity_auth.py,sha256=mQ-4pVFZRc9uweFqi7LEzAUtzhuz5fcUHVak0LSzz64,33620
14
15
  managed_deepagents/_identity_http.py,sha256=aVQfZr7UKQoXF1Dry3p9fzXIj1882sEa668rvhn37i0,16029
15
16
  managed_deepagents/_identity_links.py,sha256=Z1H2Seo2KaI-vcdU3_PGM-F06wOz0IQqUP-28wqiQsA,9172
@@ -23,12 +24,12 @@ managed_deepagents/_memory_instructions.py,sha256=lyxKfNuXrWih574gmy3NHhIq_hIpJI
23
24
  managed_deepagents/_validated_token.py,sha256=On96IOQTDz1KE_ZZmuTqY8IPw-cJ3J2tXkkmrfyvbB4,18762
24
25
  managed_deepagents/define_deep_agent.py,sha256=17VfiiBINyhlqlATHrlJRPihKVlBsDGDP8K4-fxjxU8,5611
25
26
  managed_deepagents/define_memory.py,sha256=n7Ya20dWGvwbZOkPIXoa68nTnJie8tpKEkRwfJQt_fU,5036
26
- managed_deepagents/namespaces.py,sha256=GS76G19-V5Svfpbca-9xhQfJ6vel52jcJyfj6loLt_Y,4199
27
+ managed_deepagents/namespaces.py,sha256=69i9hHAn9_jq76PPb2kPlbEpCbMylXUS5xPRDO89JFo,4461
27
28
  managed_deepagents/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
28
29
  managed_deepagents/runtime.py,sha256=B4iKTJZD0zXx_MZ09pqQFOkVo07tqJDT3qvOBr-0iOk,65782
29
30
  managed_deepagents/sandbox.py,sha256=xlpcWa6CoVX6GKip2nK-0Lw-HBCoFGFYm0r4x9eYwcY,2530
30
31
  managed_deepagents/schedules.py,sha256=MtTQN4tlm1NizFDCjZZv_huGfbQx-dj9WP1dIBybCug,5237
31
- managed_deepagents/_binaries/mda.exe,sha256=AhDKOXxB5if1NeCBHM_BIOMXZjEbmVPLflbDR9g4Tc0,3905536
32
+ managed_deepagents/_binaries/mda.exe,sha256=-FaDHv0r2g4hD7ZocjF5hp_XhuyRgckqRtkSaTRXVC8,3929600
32
33
  managed_deepagents/_channels/__init__.py,sha256=Z6eXmzaLxG09DSyJoY_CX34axcsBo9gSJ0G7pRm2D6M,1216
33
34
  managed_deepagents/_channels/assistant_text.py,sha256=f-Z3J3PlQFXZHztgBxlRoqB2LigU1Hj1YpwoXBMTR18,2171
34
35
  managed_deepagents/_channels/binding.py,sha256=6vAKbWpg-eyLErJ0SsozH4mZc50hA1NhquM63mVmWDQ,3423
@@ -53,10 +54,10 @@ managed_deepagents/_channels/trigger/client.py,sha256=cM97gzYWUTFC9o3uoveFe9-HxZ
53
54
  managed_deepagents/_channels/trigger/http.py,sha256=75ROtdT7PacKAFa-KFFRPATkHDgC7Pu48AUcipTYXY0,16903
54
55
  managed_deepagents/_channels/trigger/identity_context.py,sha256=oo6sgtNDLGGkZCzrkrifMHkkAqxj7ui2HgUUdjNA1iU,7701
55
56
  managed_deepagents/_channels/trigger/types.py,sha256=9nlArhLf-KgYidxwzrtBVjcpPlV6ytriPX3lJvPR3fE,6746
56
- managed_deepagents/_connectors/__init__.py,sha256=c_SdH3T7T6bgX7-I5jxAqDzHojmqVUCX0AXRI7g5Ovs,575
57
- managed_deepagents/_connectors/tool_selection.py,sha256=xMvohEvg_rguQvziUXzxKn4V140mmearZHel97F8tVs,2978
58
- managed_deepagents/_connectors/mcp/__init__.py,sha256=3AL429g5djYEYuiuSqsxj2VESpTkyU2NeBIJsHlWVk4,11162
59
- managed_deepagents/_connectors/mcp/load.py,sha256=4gHGQYwC8rfC_7b0YUtQ2tFYJWzzcKiFm1vz6wqDdV0,14696
57
+ managed_deepagents/_connectors/__init__.py,sha256=Ei-DvDmA-F2Bord6WuM7UtWtnuVt3u7CRSPKFqpHFhA,537
58
+ managed_deepagents/_connectors/tool_selection.py,sha256=wHA8TJqsnEw2KIzxN7JpdkD_myh8_yFI0T7Ab349_q4,2974
59
+ managed_deepagents/_connectors/mcp/__init__.py,sha256=uzGndgDbyMD5bKl2o5EYwBTgNcTkymOOitHr4rMdddc,12222
60
+ managed_deepagents/_connectors/mcp/load.py,sha256=Gim2WN-hNUXF3vNfpffnGxuDIA6CXCV7qhZ5TG5Tf7c,14665
60
61
  managed_deepagents/_memory/__init__.py,sha256=el08UQseoWgsscuE3h5_ygar0kKfwvBMFqgFLq8F3Ug,268
61
62
  managed_deepagents/_memory/async_util.py,sha256=SXre1vvF11-v9G1L6m45xb2fDIWsa08qPxFQ0MGwIgE,1463
62
63
  managed_deepagents/_memory/context_io.py,sha256=Ez3K0PH1tjlcHn3-s44JhyDjf1NvDnvbmcNiRd6Pqpc,1621
@@ -69,9 +70,9 @@ managed_deepagents/identity/__init__.py,sha256=RKuFKCN3Ui2ggUzdntYhnEdZRf-vT1khU
69
70
  managed_deepagents/identity/claims.py,sha256=prby7r_8Lww3MmoBm8PX6PsSGE3GwSHkr8mVIz_08nU,1180
70
71
  managed_deepagents/identity/options.py,sha256=x_yMbXEFR7zXyehEjDHG2qjAlukXjrRBcQLcM6IndLM,2550
71
72
  managed_deepagents/identity/supabase.py,sha256=8uobl3WCrQL_ieSXG8KTLf1th7Qjim9liZT50HeaJE0,6026
72
- managed_deepagents-0.6.2.dev53.dist-info/licenses/LICENSE,sha256=WnPZQQ608ojZUkXl7BW6VbKloIAibZiGm1K6cjeZOL8,1066
73
- managed_deepagents-0.6.2.dev53.dist-info/METADATA,sha256=Rb-W6mSRGEnhbHjpyI69y5hgBuO04EOxyxdcPNTHCpg,13690
74
- managed_deepagents-0.6.2.dev53.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
75
- managed_deepagents-0.6.2.dev53.dist-info/entry_points.txt,sha256=ziZqkKuaDO3qHCjKmBhrx9cm3ok8bZ9xcEDpUTj4unM,53
76
- managed_deepagents-0.6.2.dev53.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
77
- managed_deepagents-0.6.2.dev53.dist-info/RECORD,,
73
+ managed_deepagents-0.6.2.dev55.dist-info/licenses/LICENSE,sha256=WnPZQQ608ojZUkXl7BW6VbKloIAibZiGm1K6cjeZOL8,1066
74
+ managed_deepagents-0.6.2.dev55.dist-info/METADATA,sha256=atyu0CKHLYl_3TFm1yHep-LQJkh59vMs1J_2xw37kU4,13887
75
+ managed_deepagents-0.6.2.dev55.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
76
+ managed_deepagents-0.6.2.dev55.dist-info/entry_points.txt,sha256=ziZqkKuaDO3qHCjKmBhrx9cm3ok8bZ9xcEDpUTj4unM,53
77
+ managed_deepagents-0.6.2.dev55.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
78
+ managed_deepagents-0.6.2.dev55.dist-info/RECORD,,