managed-deepagents 0.6.2.dev29__py3-none-win_amd64.whl → 0.6.2.dev30__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 +8 -8
- managed_deepagents/_agent_auth.py +16 -184
- managed_deepagents/_agent_auth_http.py +5 -21
- managed_deepagents/_binaries/mda.exe +0 -0
- managed_deepagents/_channels/manifest.py +0 -2
- managed_deepagents/_channels/trigger/http.py +1 -1
- managed_deepagents/_connectors/mcp/__init__.py +20 -49
- managed_deepagents/_connectors/mcp/load.py +43 -50
- managed_deepagents/_credential_gate.py +89 -86
- managed_deepagents/{_connections.py → _credentials.py} +146 -171
- {managed_deepagents-0.6.2.dev29.dist-info → managed_deepagents-0.6.2.dev30.dist-info}/METADATA +10 -13
- {managed_deepagents-0.6.2.dev29.dist-info → managed_deepagents-0.6.2.dev30.dist-info}/RECORD +16 -16
- {managed_deepagents-0.6.2.dev29.dist-info → managed_deepagents-0.6.2.dev30.dist-info}/WHEEL +0 -0
- {managed_deepagents-0.6.2.dev29.dist-info → managed_deepagents-0.6.2.dev30.dist-info}/entry_points.txt +0 -0
- {managed_deepagents-0.6.2.dev29.dist-info → managed_deepagents-0.6.2.dev30.dist-info}/licenses/LICENSE +0 -0
- {managed_deepagents-0.6.2.dev29.dist-info → managed_deepagents-0.6.2.dev30.dist-info}/top_level.txt +0 -0
managed_deepagents/__init__.py
CHANGED
|
@@ -9,18 +9,18 @@ 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
|
+
``credentials.get(...)``, 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.
|
|
19
|
-
ConnectionNotFoundError,
|
|
18
|
+
from managed_deepagents._credentials import (
|
|
20
19
|
CredentialAuthorizationRequiredError,
|
|
20
|
+
CredentialNotFoundError,
|
|
21
|
+
CredentialOwner,
|
|
21
22
|
CredentialStoreError,
|
|
22
|
-
|
|
23
|
-
connections,
|
|
23
|
+
credentials,
|
|
24
24
|
)
|
|
25
25
|
from managed_deepagents._managed_tools import (
|
|
26
26
|
ManagedDeepAgentRuntime,
|
|
@@ -38,19 +38,19 @@ from managed_deepagents.sandbox import SandboxDefinition
|
|
|
38
38
|
from managed_deepagents.schedules import define_schedule
|
|
39
39
|
|
|
40
40
|
__all__ = [
|
|
41
|
-
"ConnectionNotFoundError",
|
|
42
41
|
"CredentialAuthorizationRequiredError",
|
|
42
|
+
"CredentialNotFoundError",
|
|
43
|
+
"CredentialOwner",
|
|
43
44
|
"CredentialStoreError",
|
|
44
45
|
"DeepAgentDefinition",
|
|
45
|
-
"InvalidAgentAuthEndpointError",
|
|
46
46
|
"ManagedDeepAgentRuntime",
|
|
47
47
|
"ManagedServerInfo",
|
|
48
48
|
"MemoryDefinition",
|
|
49
49
|
"SandboxDefinition",
|
|
50
50
|
"auth",
|
|
51
51
|
"channels",
|
|
52
|
-
"connections",
|
|
53
52
|
"connectors",
|
|
53
|
+
"credentials",
|
|
54
54
|
"define_deep_agent",
|
|
55
55
|
"define_identity",
|
|
56
56
|
"define_memory",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Agent Auth client used by ``
|
|
1
|
+
"""Agent Auth client used by ``credentials.get`` and the pre-run credential gate.
|
|
2
2
|
|
|
3
3
|
Talks to the shipped Agent Auth APIs (credentials + oauth-authorization-sessions),
|
|
4
4
|
not the aspirational ``credentials/mint`` endpoint.
|
|
@@ -33,7 +33,6 @@ __all__ = [
|
|
|
33
33
|
"AgentAuthUnknownProviderError",
|
|
34
34
|
"CredentialAuthorizationRequiredError",
|
|
35
35
|
"PendingAuthorization",
|
|
36
|
-
"WorkspaceConnection",
|
|
37
36
|
]
|
|
38
37
|
|
|
39
38
|
|
|
@@ -46,29 +45,20 @@ class AgentAuthContext:
|
|
|
46
45
|
principal_id: str
|
|
47
46
|
api_key: str | None = None
|
|
48
47
|
workspace_id: str | None = None
|
|
49
|
-
#: Agent Builder service JWT for workspace-owned credential material.
|
|
50
|
-
service_key: str | None = None
|
|
51
48
|
|
|
52
49
|
|
|
53
50
|
class PendingAuthorization(TypedDict):
|
|
54
|
-
"""One
|
|
51
|
+
"""One credential slug the caller has not granted, with its connect URL."""
|
|
55
52
|
|
|
56
53
|
slug: str
|
|
57
54
|
connect_url: str
|
|
58
55
|
auth_id: str
|
|
59
56
|
|
|
60
57
|
|
|
61
|
-
@dataclass(frozen=True)
|
|
62
|
-
class WorkspaceConnection:
|
|
63
|
-
connection_id: str
|
|
64
|
-
kind: str
|
|
65
|
-
owner_policy: str
|
|
66
|
-
|
|
67
|
-
|
|
68
58
|
class CredentialAuthorizationRequiredError(RuntimeError):
|
|
69
59
|
def __init__(self, slugs: list[str]) -> None:
|
|
70
60
|
super().__init__(
|
|
71
|
-
f"the caller has not connected: {', '.join(slugs)}. Declare the
|
|
61
|
+
f"the caller has not connected: {', '.join(slugs)}. Declare the credential "
|
|
72
62
|
"on a connector so the pre-run gate can request it."
|
|
73
63
|
)
|
|
74
64
|
self.slugs = slugs
|
|
@@ -116,124 +106,6 @@ class AgentAuth:
|
|
|
116
106
|
self._timeout = timeout
|
|
117
107
|
self._request: AgentAuthRequest = request if request is not None else self._urlopen_json
|
|
118
108
|
|
|
119
|
-
def find_workspace_connection(self, slug: str) -> WorkspaceConnection | None:
|
|
120
|
-
"""List workspace connections and return the row for ``slug``, if any."""
|
|
121
|
-
if not (self.context.workspace_id or "").strip():
|
|
122
|
-
raise CredentialStoreError(
|
|
123
|
-
"connections are not configured: LANGSMITH_WORKSPACE_ID is not set"
|
|
124
|
-
)
|
|
125
|
-
cursor: str | None = None
|
|
126
|
-
for _ in range(AGENT_AUTH_MAX_PROVIDER_PAGES):
|
|
127
|
-
query: dict[str, str] = {
|
|
128
|
-
"page_size": str(AGENT_AUTH_PROVIDER_PAGE_SIZE),
|
|
129
|
-
}
|
|
130
|
-
if cursor:
|
|
131
|
-
query["cursor"] = cursor
|
|
132
|
-
status, body = self._request(
|
|
133
|
-
"GET",
|
|
134
|
-
f"/v1/agent-auth/connections?{urllib.parse.urlencode(query)}",
|
|
135
|
-
)
|
|
136
|
-
if status >= _HTTP_BAD_REQUEST:
|
|
137
|
-
raise CredentialStoreError("Agent Auth list connections failed", status)
|
|
138
|
-
raw_items = body.get("items")
|
|
139
|
-
items = raw_items if isinstance(raw_items, list) else []
|
|
140
|
-
for item in items:
|
|
141
|
-
if not isinstance(item, Mapping) or item.get("slug") != slug:
|
|
142
|
-
continue
|
|
143
|
-
connection_id = (
|
|
144
|
-
item.get("id") or item.get("connection_id") or item.get("credential_id")
|
|
145
|
-
)
|
|
146
|
-
kind = _connection_credential_kind(item)
|
|
147
|
-
owner_policy = item.get("owner_policy")
|
|
148
|
-
if not isinstance(connection_id, str) or not connection_id:
|
|
149
|
-
raise CredentialStoreError("Agent Auth connection row missing id or kind")
|
|
150
|
-
if not isinstance(kind, str) or not kind:
|
|
151
|
-
raise CredentialStoreError("Agent Auth connection row missing id or kind")
|
|
152
|
-
return WorkspaceConnection(
|
|
153
|
-
connection_id=connection_id,
|
|
154
|
-
kind=kind,
|
|
155
|
-
owner_policy=str(owner_policy) if isinstance(owner_policy, str) else "",
|
|
156
|
-
)
|
|
157
|
-
next_cursor = body.get("next_cursor")
|
|
158
|
-
if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
|
|
159
|
-
return None
|
|
160
|
-
cursor = next_cursor
|
|
161
|
-
raise CredentialStoreError("Agent Auth connection list returned too many pages")
|
|
162
|
-
|
|
163
|
-
def find_workspace_credential_id(self, slug: str) -> str | None:
|
|
164
|
-
"""Workspace credential id for an agent-owned opaque secret grant."""
|
|
165
|
-
connection = self.find_workspace_connection(slug)
|
|
166
|
-
if connection is None:
|
|
167
|
-
return None
|
|
168
|
-
return self.find_secret_credential_id(connection, slug)
|
|
169
|
-
|
|
170
|
-
def find_secret_credential_id(self, connection: WorkspaceConnection, slug: str) -> str | None:
|
|
171
|
-
"""Resolve the credential id for a secret connection row.
|
|
172
|
-
|
|
173
|
-
With an Agent Builder service key, prefer GET-for-owner / workspace list.
|
|
174
|
-
Personal API keys may only list agent-owned credentials.
|
|
175
|
-
"""
|
|
176
|
-
if connection.kind != "secret":
|
|
177
|
-
return None
|
|
178
|
-
workspace_id = (self.context.workspace_id or "").strip()
|
|
179
|
-
if not workspace_id:
|
|
180
|
-
raise CredentialStoreError(
|
|
181
|
-
"connections are not configured: LANGSMITH_WORKSPACE_ID is not set"
|
|
182
|
-
)
|
|
183
|
-
if (self.context.service_key or "").strip():
|
|
184
|
-
from_owner = self._credential_id_for_owner(
|
|
185
|
-
connection.connection_id, "workspace", workspace_id
|
|
186
|
-
)
|
|
187
|
-
if from_owner is not None:
|
|
188
|
-
return from_owner
|
|
189
|
-
return self.find_credential_id(
|
|
190
|
-
owner_type="workspace",
|
|
191
|
-
owner_id=workspace_id,
|
|
192
|
-
kind="secret",
|
|
193
|
-
slug=slug,
|
|
194
|
-
)
|
|
195
|
-
return self.find_credential_id(
|
|
196
|
-
owner_type="agent",
|
|
197
|
-
owner_id=self.context.agent_id,
|
|
198
|
-
kind="secret",
|
|
199
|
-
slug=slug,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
def _credential_id_for_owner(
|
|
203
|
-
self, connection_id: str, owner_type: str, owner_id: str
|
|
204
|
-
) -> str | None:
|
|
205
|
-
query = urllib.parse.urlencode(
|
|
206
|
-
{
|
|
207
|
-
"credential_owner_type": owner_type,
|
|
208
|
-
"credential_owner_id": owner_id,
|
|
209
|
-
}
|
|
210
|
-
)
|
|
211
|
-
status, body = self._request(
|
|
212
|
-
"GET",
|
|
213
|
-
f"/v1/agent-auth/connections/{_path_segment(connection_id)}?{query}",
|
|
214
|
-
)
|
|
215
|
-
if status == _HTTP_NOT_FOUND:
|
|
216
|
-
return None
|
|
217
|
-
if status >= _HTTP_BAD_REQUEST:
|
|
218
|
-
raise CredentialStoreError("Agent Auth get connection for owner failed", status)
|
|
219
|
-
return _connection_credential_id(body)
|
|
220
|
-
|
|
221
|
-
def read_user_secret(self, slug: str) -> str | None:
|
|
222
|
-
"""Read a caller-owned opaque secret grant for a workspace connection slug."""
|
|
223
|
-
credential_id = self.find_credential_id(
|
|
224
|
-
owner_type="user",
|
|
225
|
-
owner_id=self.context.principal_id,
|
|
226
|
-
kind="secret",
|
|
227
|
-
slug=slug,
|
|
228
|
-
)
|
|
229
|
-
if credential_id is None:
|
|
230
|
-
return None
|
|
231
|
-
return self.read_secret(credential_id)
|
|
232
|
-
|
|
233
|
-
def read_connection_access_token(self, connection_id: str) -> str | None:
|
|
234
|
-
"""Read an OAuth access token stored on a workspace connection row."""
|
|
235
|
-
return self._fetch_access_token(connection_id)
|
|
236
|
-
|
|
237
109
|
def read_user_access_token(self, slug: str) -> str | None:
|
|
238
110
|
"""Read the caller's OAuth access token without starting a session.
|
|
239
111
|
|
|
@@ -256,16 +128,12 @@ class AgentAuth:
|
|
|
256
128
|
|
|
257
129
|
def start_authorization_session(self, slug: str) -> PendingAuthorization:
|
|
258
130
|
"""Start an OAuth session for a slug the caller has not granted."""
|
|
259
|
-
|
|
260
|
-
if connection is None:
|
|
261
|
-
raise CredentialStoreError(f"workspace connection {slug!r} is not configured")
|
|
262
|
-
if connection.kind != "oauth2":
|
|
263
|
-
raise CredentialStoreError(f"connection {slug!r} is not an OAuth connection")
|
|
131
|
+
provider_id = self.resolve_oauth_provider_id(slug)
|
|
264
132
|
status, body = self._request(
|
|
265
133
|
"POST",
|
|
266
134
|
"/v1/agent-auth/oauth-authorization-sessions",
|
|
267
135
|
body={
|
|
268
|
-
"
|
|
136
|
+
"provider_id": provider_id,
|
|
269
137
|
"principal_id": self.context.principal_id,
|
|
270
138
|
"agent_id": self.context.agent_id,
|
|
271
139
|
},
|
|
@@ -314,35 +182,18 @@ class AgentAuth:
|
|
|
314
182
|
) -> str | None:
|
|
315
183
|
"""Find a credential id by slug.
|
|
316
184
|
|
|
317
|
-
The
|
|
318
|
-
slug match is made client-side across pages. The credential key is
|
|
319
|
-
``credential_id``, never ``id``.
|
|
185
|
+
The credential key is ``credential_id``, never ``id``.
|
|
320
186
|
"""
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if status >= _HTTP_BAD_REQUEST:
|
|
332
|
-
raise CredentialStoreError("Agent Auth list credentials failed", status)
|
|
333
|
-
raw_items = body.get("items")
|
|
334
|
-
items = raw_items if isinstance(raw_items, list) else []
|
|
335
|
-
for item in items:
|
|
336
|
-
if not isinstance(item, Mapping) or item.get("slug") != slug:
|
|
337
|
-
continue
|
|
338
|
-
credential_id = item.get("credential_id")
|
|
339
|
-
if isinstance(credential_id, str) and credential_id:
|
|
340
|
-
return credential_id
|
|
341
|
-
next_cursor = body.get("next_cursor")
|
|
342
|
-
if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
|
|
343
|
-
return None
|
|
344
|
-
cursor = next_cursor
|
|
345
|
-
raise CredentialStoreError("Agent Auth credential list returned too many pages")
|
|
187
|
+
query = agent_auth_credential_query(
|
|
188
|
+
owner_type=owner_type, owner_id=owner_id, kind=kind, slug=slug
|
|
189
|
+
)
|
|
190
|
+
status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
|
|
191
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
192
|
+
raise CredentialStoreError("Agent Auth list credentials failed", status)
|
|
193
|
+
items = body.get("items")
|
|
194
|
+
row = items[0] if isinstance(items, list) and items else None
|
|
195
|
+
credential_id = row.get("credential_id") if isinstance(row, Mapping) else None
|
|
196
|
+
return credential_id if isinstance(credential_id, str) else None
|
|
346
197
|
|
|
347
198
|
def read_secret(self, credential_id: str) -> str | None:
|
|
348
199
|
status, body = self._request(
|
|
@@ -390,7 +241,6 @@ class AgentAuth:
|
|
|
390
241
|
return agent_auth_headers(
|
|
391
242
|
api_key=self.context.api_key,
|
|
392
243
|
workspace_id=self.context.workspace_id,
|
|
393
|
-
service_key=self.context.service_key,
|
|
394
244
|
)
|
|
395
245
|
|
|
396
246
|
|
|
@@ -398,24 +248,6 @@ def _path_segment(value: str) -> str:
|
|
|
398
248
|
return urllib.parse.quote(value, safe="")
|
|
399
249
|
|
|
400
250
|
|
|
401
|
-
def _connection_credential_kind(item: Mapping[str, object]) -> str:
|
|
402
|
-
credential = item.get("credential")
|
|
403
|
-
if isinstance(credential, Mapping):
|
|
404
|
-
kind = credential.get("kind")
|
|
405
|
-
if isinstance(kind, str) and kind:
|
|
406
|
-
return kind
|
|
407
|
-
kind = item.get("kind")
|
|
408
|
-
return kind if isinstance(kind, str) else ""
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def _connection_credential_id(body: Mapping[str, object]) -> str | None:
|
|
412
|
-
credential = body.get("credential")
|
|
413
|
-
if not isinstance(credential, Mapping):
|
|
414
|
-
return None
|
|
415
|
-
credential_id = credential.get("credential_id")
|
|
416
|
-
return credential_id if isinstance(credential_id, str) and credential_id else None
|
|
417
|
-
|
|
418
|
-
|
|
419
251
|
def _is_authorization_required(status: int, body: dict[str, object]) -> bool:
|
|
420
252
|
"""Agent Auth answers 409 when a stored grant has lapsed and needs re-auth."""
|
|
421
253
|
if status != _HTTP_CONFLICT:
|
|
@@ -46,40 +46,24 @@ def agent_auth_credential_query(
|
|
|
46
46
|
owner_type: str,
|
|
47
47
|
owner_id: str,
|
|
48
48
|
kind: str,
|
|
49
|
-
|
|
50
|
-
cursor: str | None = None,
|
|
49
|
+
slug: str,
|
|
51
50
|
) -> str:
|
|
52
|
-
"""Query string
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"""
|
|
57
|
-
params: dict[str, str] = {
|
|
58
|
-
"owner_type": owner_type,
|
|
59
|
-
"owner_id": owner_id,
|
|
60
|
-
"kind": kind,
|
|
61
|
-
}
|
|
62
|
-
if page_size is not None:
|
|
63
|
-
params["page_size"] = str(page_size)
|
|
64
|
-
if cursor:
|
|
65
|
-
params["cursor"] = cursor
|
|
66
|
-
return urllib.parse.urlencode(params)
|
|
51
|
+
"""Query string that addresses one credential by its natural key."""
|
|
52
|
+
return urllib.parse.urlencode(
|
|
53
|
+
{"owner_type": owner_type, "owner_id": owner_id, "kind": kind, "slug": slug}
|
|
54
|
+
)
|
|
67
55
|
|
|
68
56
|
|
|
69
57
|
def agent_auth_headers(
|
|
70
58
|
*,
|
|
71
59
|
api_key: str | None = None,
|
|
72
60
|
workspace_id: str | None = None,
|
|
73
|
-
service_key: str | None = None,
|
|
74
61
|
) -> dict[str, str]:
|
|
75
62
|
headers = {
|
|
76
63
|
"accept": "application/json",
|
|
77
64
|
"content-type": "application/json",
|
|
78
65
|
"x-langsmith-product": "mda",
|
|
79
66
|
}
|
|
80
|
-
resolved_service_key = (service_key or "").strip()
|
|
81
|
-
if resolved_service_key:
|
|
82
|
-
headers["x-service-key"] = resolved_service_key
|
|
83
67
|
resolved_api_key = (api_key or "").strip()
|
|
84
68
|
if resolved_api_key:
|
|
85
69
|
headers["x-api-key"] = resolved_api_key
|
|
Binary file
|
|
@@ -17,8 +17,6 @@ JsonObject = dict[str, JsonValue]
|
|
|
17
17
|
class _OptionalChannelManifest(TypedDict, total=False):
|
|
18
18
|
transport: Literal["trigger_server"]
|
|
19
19
|
userCredentials: list[str]
|
|
20
|
-
userConnections: list[str]
|
|
21
|
-
agentConnections: list[str]
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
class ChannelManifest(_OptionalChannelManifest):
|
|
@@ -250,7 +250,7 @@ async def _observe_accepted_run(
|
|
|
250
250
|
event["channel_id"],
|
|
251
251
|
{
|
|
252
252
|
"type": "post",
|
|
253
|
-
"action_id":
|
|
253
|
+
"action_id": str(uuid.uuid4()),
|
|
254
254
|
"address": event["address"],
|
|
255
255
|
"source_thread_key": event["source_thread_key"],
|
|
256
256
|
"message": {"text": text},
|
|
@@ -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, credentials
|
|
13
13
|
|
|
14
14
|
connector = connectors.mcp(
|
|
15
15
|
use_standard_content_blocks=True,
|
|
@@ -24,7 +24,7 @@ Example::
|
|
|
24
24
|
"notion": {
|
|
25
25
|
"transport": "http",
|
|
26
26
|
"url": "https://mcp.notion.com/mcp",
|
|
27
|
-
"
|
|
27
|
+
"credential": credentials.get("notion", {"type": "user"}),
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
30
|
)
|
|
@@ -33,7 +33,6 @@ Example::
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
35
|
import math
|
|
36
|
-
import warnings
|
|
37
36
|
from collections.abc import Mapping, Sequence
|
|
38
37
|
from typing import TYPE_CHECKING, Annotated, Any, Literal, TypedDict, cast
|
|
39
38
|
|
|
@@ -46,9 +45,9 @@ from pydantic import (
|
|
|
46
45
|
model_validator,
|
|
47
46
|
)
|
|
48
47
|
|
|
49
|
-
from managed_deepagents._connections import ConnectionReference, is_connection_reference
|
|
50
48
|
from managed_deepagents._connector import Connector
|
|
51
49
|
from managed_deepagents._connectors.mcp.load import load_managed_mcp_tools
|
|
50
|
+
from managed_deepagents._credentials import CredentialReference
|
|
52
51
|
from managed_deepagents._extension import define_extension
|
|
53
52
|
|
|
54
53
|
if TYPE_CHECKING:
|
|
@@ -63,9 +62,9 @@ class McpToolSelectionConfig(TypedDict, total=False):
|
|
|
63
62
|
class RemoteMcpServerBaseConfig(McpToolSelectionConfig, total=False):
|
|
64
63
|
url: str
|
|
65
64
|
headers: dict[str, str]
|
|
66
|
-
#: Bearer
|
|
67
|
-
#: at load, a user owner is the caller's grant loaded after the gate.
|
|
68
|
-
|
|
65
|
+
#: Bearer credential: an agent owner is a deployment-wide secret read once
|
|
66
|
+
#: at load, a user owner is the caller's OAuth grant loaded after the gate.
|
|
67
|
+
credential: CredentialReference
|
|
69
68
|
#: Default timeout in seconds for tools loaded from this server.
|
|
70
69
|
default_tool_timeout: float
|
|
71
70
|
|
|
@@ -124,26 +123,20 @@ class McpServersDefinition(Connector):
|
|
|
124
123
|
return load_managed_mcp_tools(self)
|
|
125
124
|
|
|
126
125
|
def requirements(self, name: str) -> ChannelManifest:
|
|
127
|
-
"""Describe user
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if connection.owner["type"] == "user":
|
|
135
|
-
user_slugs.add(connection.slug)
|
|
136
|
-
else:
|
|
137
|
-
agent_slugs.add(connection.slug)
|
|
126
|
+
"""Describe user credentials that deploy must validate."""
|
|
127
|
+
slugs = {
|
|
128
|
+
credential.slug
|
|
129
|
+
for server in self.config["mcp_servers"].values()
|
|
130
|
+
if isinstance((credential := server.get("credential")), CredentialReference)
|
|
131
|
+
and credential.owner["type"] == "user"
|
|
132
|
+
}
|
|
138
133
|
return {
|
|
139
134
|
"name": name,
|
|
140
135
|
"provider": "mcp",
|
|
141
136
|
"autoReply": False,
|
|
142
137
|
"requiredPermissions": [],
|
|
143
138
|
"requiredEnv": [],
|
|
144
|
-
"
|
|
145
|
-
"agentConnections": sorted(agent_slugs),
|
|
146
|
-
"userCredentials": sorted(user_slugs),
|
|
139
|
+
"userCredentials": sorted(slugs),
|
|
147
140
|
"providerConfig": {},
|
|
148
141
|
}
|
|
149
142
|
|
|
@@ -159,12 +152,6 @@ _STDIO_REJECTION = (
|
|
|
159
152
|
"HTTP or write a normal authored tool instead."
|
|
160
153
|
)
|
|
161
154
|
|
|
162
|
-
_CREDENTIAL_FIELD_DEPRECATION = (
|
|
163
|
-
'the "credential" MCP server field is deprecated; use "connection" instead'
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
_CREDENTIAL_FIELD_CONFLICT = 'use either "connection" or "credential", not both'
|
|
167
|
-
|
|
168
155
|
|
|
169
156
|
def _non_empty_tool_name(name: str) -> str:
|
|
170
157
|
if not name.strip():
|
|
@@ -197,16 +184,7 @@ class _RemoteMcpServer(BaseModel):
|
|
|
197
184
|
default_tool_timeout: float | None = None
|
|
198
185
|
include_tools: list[_ToolName] | None = None
|
|
199
186
|
exclude_tools: list[_ToolName] | None = None
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
@field_validator("connection")
|
|
203
|
-
@classmethod
|
|
204
|
-
def _valid_connection(
|
|
205
|
-
cls, connection: ConnectionReference | None
|
|
206
|
-
) -> ConnectionReference | None:
|
|
207
|
-
if connection is not None and not is_connection_reference(connection):
|
|
208
|
-
raise ValueError("must be a connections.get(slug, owner) reference")
|
|
209
|
-
return connection
|
|
187
|
+
credential: CredentialReference | None = None
|
|
210
188
|
|
|
211
189
|
@field_validator("default_tool_timeout")
|
|
212
190
|
@classmethod
|
|
@@ -217,19 +195,12 @@ class _RemoteMcpServer(BaseModel):
|
|
|
217
195
|
|
|
218
196
|
@model_validator(mode="before")
|
|
219
197
|
@classmethod
|
|
220
|
-
def
|
|
221
|
-
if
|
|
222
|
-
|
|
223
|
-
|
|
198
|
+
def _reject_stdio(cls, server: Any) -> Any:
|
|
199
|
+
if isinstance(server, Mapping) and (
|
|
200
|
+
server.get("transport") == "stdio" or "command" in server
|
|
201
|
+
):
|
|
224
202
|
raise ValueError(_STDIO_REJECTION)
|
|
225
|
-
|
|
226
|
-
return server
|
|
227
|
-
if "connection" in server:
|
|
228
|
-
raise ValueError(_CREDENTIAL_FIELD_CONFLICT)
|
|
229
|
-
warnings.warn(_CREDENTIAL_FIELD_DEPRECATION, DeprecationWarning, stacklevel=1)
|
|
230
|
-
normalized = dict(server)
|
|
231
|
-
normalized["connection"] = normalized.pop("credential")
|
|
232
|
-
return normalized
|
|
203
|
+
return server
|
|
233
204
|
|
|
234
205
|
@field_validator("transport", mode="before")
|
|
235
206
|
@classmethod
|