assistant-runtime-sdk 1.7.1__tar.gz → 1.9.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assistant_runtime_sdk
3
- Version: 1.7.1
3
+ Version: 1.9.0
4
4
  Summary: Python SDK for FAC Cloud - the Assistant Runtime AI assistant backend
5
5
  Keywords: frappe,ai,assistant,llm,mcp,anthropic,openai,streaming
6
6
  Author: Paul Clinton
@@ -53,7 +53,7 @@ Standalone Functions:
53
53
 
54
54
  """
55
55
 
56
- __version__ = "1.7.1"
56
+ __version__ = "1.9.0"
57
57
  __author__ = "Paul Clinton"
58
58
  __license__ = "AGPL-3.0"
59
59
 
@@ -1341,7 +1341,7 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1341
1341
  user_id: str,
1342
1342
  server_name: str,
1343
1343
  endpoint_url: str,
1344
- transport_type: str = "SSE",
1344
+ transport_type: str = "HTTP",
1345
1345
  auth_type: str = "OAuth",
1346
1346
  oauth_client_id: Optional[str] = None,
1347
1347
  oauth_client_secret: Optional[str] = None,
@@ -1352,12 +1352,14 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1352
1352
  api_key_header: str = "Authorization",
1353
1353
  allowed_tools: Optional[list] = None,
1354
1354
  blocked_tools: Optional[list] = None,
1355
+ managed: bool = False,
1355
1356
  ) -> Dict[str, Any]:
1356
1357
  """Async version of AssistantRuntimeClient.add_user_mcp_server."""
1357
1358
  endpoint, params = self._prepare_add_user_mcp_server(
1358
1359
  user_id, server_name, endpoint_url, transport_type, auth_type,
1359
1360
  oauth_client_id, oauth_client_secret, access_token, refresh_token,
1360
1361
  token_expires_in, api_key, api_key_header, allowed_tools, blocked_tools,
1362
+ managed,
1361
1363
  )
1362
1364
  return await self._request_post_form(endpoint, params)
1363
1365
 
@@ -1367,7 +1369,7 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1367
1369
  try:
1368
1370
  return await self._request_get(endpoint, params)
1369
1371
  except Exception as e:
1370
- return {"user_id": user_id, "mcp_servers": [], "error": str(e)}
1372
+ return {"user_id": user_id, "mcp_servers": [], "error": str(e), "_ar_unreachable": True}
1371
1373
 
1372
1374
  async def update_mcp_server_tokens(
1373
1375
  self,
@@ -1386,6 +1388,64 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1386
1388
  endpoint, params = self._prepare_remove_user_mcp_server(user_id, server_name)
1387
1389
  return await self._request_delete(endpoint, params)
1388
1390
 
1391
+ async def enable_mcp_server(self, user_id: str, server_name: str, enabled: bool = True) -> Dict[str, Any]:
1392
+ """Async version of AssistantRuntimeClient.enable_mcp_server."""
1393
+ endpoint, params = self._prepare_enable_mcp_server(user_id, server_name, enabled)
1394
+ return await self._request_post_form(endpoint, params)
1395
+
1396
+ async def test_mcp_server(self, user_id: str, server_name: str) -> Dict[str, Any]:
1397
+ """Async version of AssistantRuntimeClient.test_mcp_server."""
1398
+ endpoint, params = self._prepare_test_mcp_server(user_id, server_name)
1399
+ return await self._request_post_form(endpoint, params)
1400
+
1401
+ async def set_mcp_server_tools(
1402
+ self,
1403
+ user_id: str,
1404
+ server_name: str,
1405
+ allowed_tools: Optional[list] = None,
1406
+ blocked_tools: Optional[list] = None,
1407
+ ) -> Dict[str, Any]:
1408
+ """Async version of AssistantRuntimeClient.set_mcp_server_tools."""
1409
+ endpoint, params = self._prepare_set_mcp_server_tools(
1410
+ user_id, server_name, allowed_tools, blocked_tools
1411
+ )
1412
+ return await self._request_post_form(endpoint, params)
1413
+
1414
+ async def begin_mcp_connect(
1415
+ self,
1416
+ endpoint_url: str,
1417
+ user_id: str,
1418
+ client_id: Optional[str] = None,
1419
+ client_secret: Optional[str] = None,
1420
+ ) -> Dict[str, Any]:
1421
+ """Async version of AssistantRuntimeClient.begin_mcp_connect."""
1422
+ endpoint, params = self._prepare_begin_mcp_connect(
1423
+ user_id, endpoint_url, client_id, client_secret
1424
+ )
1425
+ return await self._request_post_form(endpoint, params)
1426
+
1427
+ async def get_mcp_connect_session(self, handle: str, user_id: str) -> Dict[str, Any]:
1428
+ """Async version of AssistantRuntimeClient.get_mcp_connect_session."""
1429
+ endpoint, params = self._prepare_get_mcp_connect_session(user_id, handle)
1430
+ return await self._request_get(endpoint, params)
1431
+
1432
+ async def commit_mcp_connect(
1433
+ self, handle: str, server_name: str, user_id: str
1434
+ ) -> Dict[str, Any]:
1435
+ """Async version of AssistantRuntimeClient.commit_mcp_connect."""
1436
+ endpoint, params = self._prepare_commit_mcp_connect(user_id, handle, server_name)
1437
+ return await self._request_post_form(endpoint, params)
1438
+
1439
+ async def abandon_mcp_connect(self, handle: str, user_id: str) -> Dict[str, Any]:
1440
+ """Async version of AssistantRuntimeClient.abandon_mcp_connect."""
1441
+ endpoint, params = self._prepare_abandon_mcp_connect(user_id, handle)
1442
+ return await self._request_post_form(endpoint, params)
1443
+
1444
+ async def begin_mcp_reauth(self, server_name: str, user_id: str) -> Dict[str, Any]:
1445
+ """Async version of AssistantRuntimeClient.begin_mcp_reauth."""
1446
+ endpoint, params = self._prepare_begin_mcp_reauth(user_id, server_name)
1447
+ return await self._request_post_form(endpoint, params)
1448
+
1389
1449
  async def list_users(
1390
1450
  self,
1391
1451
  status: Optional[str] = None,
@@ -1437,7 +1437,7 @@ class BaseAssistantRuntimeClient:
1437
1437
 
1438
1438
  def _prepare_add_user_mcp_server(
1439
1439
  self, user_id: str, server_name: str, endpoint_url: str,
1440
- transport_type: str = "SSE", auth_type: str = "OAuth",
1440
+ transport_type: str = "HTTP", auth_type: str = "OAuth",
1441
1441
  oauth_client_id: Optional[str] = None,
1442
1442
  oauth_client_secret: Optional[str] = None,
1443
1443
  access_token: Optional[str] = None,
@@ -1447,6 +1447,7 @@ class BaseAssistantRuntimeClient:
1447
1447
  api_key_header: str = "Authorization",
1448
1448
  allowed_tools: Optional[list] = None,
1449
1449
  blocked_tools: Optional[list] = None,
1450
+ managed: bool = False,
1450
1451
  ) -> tuple:
1451
1452
  params: Dict[str, Any] = {
1452
1453
  "tenant_id": self.tenant_id,
@@ -1474,6 +1475,7 @@ class BaseAssistantRuntimeClient:
1474
1475
  params["allowed_tools"] = json.dumps(allowed_tools)
1475
1476
  if blocked_tools:
1476
1477
  params["blocked_tools"] = json.dumps(blocked_tools)
1478
+ params["managed"] = "1" if managed else "0"
1477
1479
  return "users.add_user_mcp_server", params
1478
1480
 
1479
1481
  def _prepare_get_user_mcp_servers(self, user_id: str) -> tuple:
@@ -1504,6 +1506,94 @@ class BaseAssistantRuntimeClient:
1504
1506
  "server_name": server_name,
1505
1507
  }
1506
1508
 
1509
+ def _prepare_enable_mcp_server(
1510
+ self, user_id: str, server_name: str, enabled: bool = True
1511
+ ) -> tuple:
1512
+ return "users.enable_mcp_server", {
1513
+ "tenant_id": self.tenant_id,
1514
+ "user_id": str(user_id),
1515
+ "server_name": str(server_name),
1516
+ "enabled": "1" if enabled else "0",
1517
+ }
1518
+
1519
+ def _prepare_test_mcp_server(self, user_id: str, server_name: str) -> tuple:
1520
+ return "users.test_mcp_server", {
1521
+ "tenant_id": self.tenant_id,
1522
+ "user_id": str(user_id),
1523
+ "server_name": str(server_name),
1524
+ }
1525
+
1526
+ def _prepare_set_mcp_server_tools(
1527
+ self, user_id: str, server_name: str,
1528
+ allowed_tools: Optional[list] = None,
1529
+ blocked_tools: Optional[list] = None,
1530
+ ) -> tuple:
1531
+ params: Dict[str, Any] = {
1532
+ "tenant_id": self.tenant_id,
1533
+ "user_id": user_id,
1534
+ "server_name": server_name,
1535
+ }
1536
+ if allowed_tools is not None:
1537
+ params["allowed_tools"] = json.dumps(allowed_tools)
1538
+ if blocked_tools is not None:
1539
+ params["blocked_tools"] = json.dumps(blocked_tools)
1540
+ return "users.set_mcp_server_tools", params
1541
+
1542
+ # -------------------------------------------------------------------------
1543
+ # MCP Connect wizard (assistant_runtime.api.mcp_oauth)
1544
+ # -------------------------------------------------------------------------
1545
+
1546
+ def _prepare_begin_mcp_connect(
1547
+ self, user_id: str, endpoint_url: str,
1548
+ client_id: Optional[str] = None,
1549
+ client_secret: Optional[str] = None,
1550
+ ) -> tuple:
1551
+ params: Dict[str, Any] = {
1552
+ "tenant_id": self.tenant_id,
1553
+ "user_id": str(user_id),
1554
+ "endpoint_url": str(endpoint_url),
1555
+ }
1556
+ # Omitted, not blank: a blank client_id on the wire makes AR record an
1557
+ # empty manual registration instead of taking the DCR path.
1558
+ if client_id:
1559
+ params["client_id"] = str(client_id)
1560
+ if client_secret:
1561
+ params["client_secret"] = str(client_secret)
1562
+ return "mcp_oauth.begin_mcp_connect", params
1563
+
1564
+ def _prepare_get_mcp_connect_session(self, user_id: str, handle: str) -> tuple:
1565
+ return "mcp_oauth.get_mcp_connect_session", {
1566
+ "tenant_id": self.tenant_id,
1567
+ "user_id": str(user_id),
1568
+ "handle": str(handle),
1569
+ }
1570
+
1571
+ def _prepare_commit_mcp_connect(
1572
+ self, user_id: str, handle: str, server_name: str
1573
+ ) -> tuple:
1574
+ return "mcp_oauth.commit_mcp_connect", {
1575
+ "tenant_id": self.tenant_id,
1576
+ "user_id": str(user_id),
1577
+ "handle": str(handle),
1578
+ "server_name": str(server_name),
1579
+ }
1580
+
1581
+ def _prepare_abandon_mcp_connect(self, user_id: str, handle: str) -> tuple:
1582
+ return "mcp_oauth.abandon_mcp_connect", {
1583
+ "tenant_id": self.tenant_id,
1584
+ "user_id": str(user_id),
1585
+ "handle": str(handle),
1586
+ }
1587
+
1588
+ def _prepare_begin_mcp_reauth(self, user_id: str, server_name: str) -> tuple:
1589
+ # No endpoint_url: AR reads it from the row named here, and commit
1590
+ # updates that row in place via the session's reauth_target.
1591
+ return "mcp_oauth.begin_mcp_reauth", {
1592
+ "tenant_id": self.tenant_id,
1593
+ "user_id": str(user_id),
1594
+ "server_name": str(server_name),
1595
+ }
1596
+
1507
1597
  def _prepare_list_users(
1508
1598
  self, status: Optional[str] = None, limit: int = 50,
1509
1599
  offset: int = 0, include_mcp_count: bool = True,
@@ -2021,7 +2021,7 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2021
2021
  user_id: str,
2022
2022
  server_name: str,
2023
2023
  endpoint_url: str,
2024
- transport_type: str = "SSE",
2024
+ transport_type: str = "HTTP",
2025
2025
  auth_type: str = "OAuth",
2026
2026
  oauth_client_id: Optional[str] = None,
2027
2027
  oauth_client_secret: Optional[str] = None,
@@ -2032,12 +2032,19 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2032
2032
  api_key_header: str = "Authorization",
2033
2033
  allowed_tools: Optional[list] = None,
2034
2034
  blocked_tools: Optional[list] = None,
2035
+ managed: bool = False,
2035
2036
  ) -> Dict[str, Any]:
2036
- """Add or update an MCP server for a user."""
2037
+ """Add or update an MCP server for a user.
2038
+
2039
+ ``managed`` is sent as a hint only — AR derives the authoritative
2040
+ value server-side from whether server_name/endpoint_url match FAC's
2041
+ real registration shape, regardless of what is passed here.
2042
+ """
2037
2043
  endpoint, params = self._prepare_add_user_mcp_server(
2038
2044
  user_id, server_name, endpoint_url, transport_type, auth_type,
2039
2045
  oauth_client_id, oauth_client_secret, access_token, refresh_token,
2040
2046
  token_expires_in, api_key, api_key_header, allowed_tools, blocked_tools,
2047
+ managed,
2041
2048
  )
2042
2049
  return self._request_post_form(endpoint, params)
2043
2050
 
@@ -2047,7 +2054,7 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2047
2054
  try:
2048
2055
  return self._request_get(endpoint, params)
2049
2056
  except Exception as e:
2050
- return {"user_id": user_id, "mcp_servers": [], "error": str(e)}
2057
+ return {"user_id": user_id, "mcp_servers": [], "error": str(e), "_ar_unreachable": True}
2051
2058
 
2052
2059
  def update_mcp_server_tokens(
2053
2060
  self,
@@ -2066,6 +2073,113 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2066
2073
  endpoint, params = self._prepare_remove_user_mcp_server(user_id, server_name)
2067
2074
  return self._request_delete(endpoint, params)
2068
2075
 
2076
+ def enable_mcp_server(self, user_id: str, server_name: str, enabled: bool = True) -> Dict[str, Any]:
2077
+ """Enable or disable an MCP server without deleting it."""
2078
+ endpoint, params = self._prepare_enable_mcp_server(user_id, server_name, enabled)
2079
+ return self._request_post_form(endpoint, params)
2080
+
2081
+ def test_mcp_server(self, user_id: str, server_name: str) -> Dict[str, Any]:
2082
+ """Open a real authenticated MCP session and report what it exposes.
2083
+
2084
+ Unlike a reachability probe, this speaks MCP with the user's stored
2085
+ credentials and lists tools. Returns a result dict rather than
2086
+ raising: {"success": bool, "tool_count": int, "tools": [str], "error": str | None}.
2087
+ """
2088
+ endpoint, params = self._prepare_test_mcp_server(user_id, server_name)
2089
+ return self._request_post_form(endpoint, params)
2090
+
2091
+ def set_mcp_server_tools(
2092
+ self,
2093
+ user_id: str,
2094
+ server_name: str,
2095
+ allowed_tools: Optional[list] = None,
2096
+ blocked_tools: Optional[list] = None,
2097
+ ) -> Dict[str, Any]:
2098
+ """Replace the tool filters on one server, without touching enabled,
2099
+ status, or credentials.
2100
+
2101
+ Unlike ``add_user_mcp_server``, this is a targeted write, not an
2102
+ upsert — safe to call from a checkbox UI without risking a
2103
+ disabled server being silently re-enabled or its stored API key
2104
+ being dropped.
2105
+ """
2106
+ endpoint, params = self._prepare_set_mcp_server_tools(
2107
+ user_id, server_name, allowed_tools, blocked_tools
2108
+ )
2109
+ return self._request_post_form(endpoint, params)
2110
+
2111
+ def begin_mcp_connect(
2112
+ self,
2113
+ endpoint_url: str,
2114
+ user_id: str,
2115
+ client_id: Optional[str] = None,
2116
+ client_secret: Optional[str] = None,
2117
+ ) -> Dict[str, Any]:
2118
+ """Start a server-side MCP connect attempt and run the preflight.
2119
+
2120
+ Returns AR's connect-session payload:
2121
+ ``{"handle", "session", "preflight", "authorize_url", "redirect_uri"}``.
2122
+ ``preflight`` is ``run_preflight``'s dict — ``steps`` (each
2123
+ ``pass|fail|skipped``), ``server_info``, ``auth``. ``redirect_uri`` is
2124
+ the URI a user must register by hand when the authorization server has
2125
+ no Dynamic Client Registration.
2126
+
2127
+ ``client_id``/``client_secret`` are the no-DCR escape hatch: supply them
2128
+ to re-begin with credentials the user registered themselves.
2129
+ """
2130
+ endpoint, params = self._prepare_begin_mcp_connect(
2131
+ user_id, endpoint_url, client_id, client_secret
2132
+ )
2133
+ return self._request_post_form(endpoint, params)
2134
+
2135
+ def get_mcp_connect_session(self, handle: str, user_id: str) -> Dict[str, Any]:
2136
+ """Read an in-flight connect session by its short-lived handle.
2137
+
2138
+ Returns ``{"status", "endpoint_url", "preflight", "capabilities",
2139
+ "authorize_url", "error_message", "server_name",
2140
+ "reauth_server_name"}``. ``status`` is one of ``Preflight``,
2141
+ ``Awaiting Auth``, ``Authorized``, ``Committed``, ``Failed`` — Title
2142
+ Case with spaces, matching ``AR User MCP Server``'s own status Select.
2143
+ ``authorize_url`` is a real URL whenever one has been built, so a user
2144
+ who reloaded mid-flow can still click Connect. ``capabilities`` is
2145
+ populated only once the session reaches ``Authorized``; ``server_name``
2146
+ only once it reaches ``Committed``. ``reauth_server_name`` is the name
2147
+ of the row this session re-authorizes, or ``None`` on the ordinary add
2148
+ path — it is what lets a caller tell the two apart without carrying any
2149
+ state of its own across the authorization round trip.
2150
+ """
2151
+ endpoint, params = self._prepare_get_mcp_connect_session(user_id, handle)
2152
+ return self._request_get(endpoint, params)
2153
+
2154
+ def commit_mcp_connect(
2155
+ self, handle: str, server_name: str, user_id: str
2156
+ ) -> Dict[str, Any]:
2157
+ """Turn an authorized connect session into a real AR User MCP Server."""
2158
+ endpoint, params = self._prepare_commit_mcp_connect(user_id, handle, server_name)
2159
+ return self._request_post_form(endpoint, params)
2160
+
2161
+ def abandon_mcp_connect(self, handle: str, user_id: str) -> Dict[str, Any]:
2162
+ """Drop an in-flight connect session and the credentials escrowed in it."""
2163
+ endpoint, params = self._prepare_abandon_mcp_connect(user_id, handle)
2164
+ return self._request_post_form(endpoint, params)
2165
+
2166
+ def begin_mcp_reauth(self, server_name: str, user_id: str) -> Dict[str, Any]:
2167
+ """Re-authorize a connection that already exists.
2168
+
2169
+ Returns the same payload shape as :meth:`begin_mcp_connect`. AR opens
2170
+ the session with ``reauth_target`` pointing at the named
2171
+ ``AR User MCP Server`` row, so ``commit_mcp_connect`` updates that row's
2172
+ credentials in place and restores it to ``Active`` instead of inserting
2173
+ a second row — which is what made the duplicate-name guard fire on every
2174
+ reconnect before this endpoint existed.
2175
+
2176
+ This is the only correct entry point for reconnecting. Calling
2177
+ ``begin_mcp_connect`` and then committing under the existing name takes
2178
+ the add path and fails.
2179
+ """
2180
+ endpoint, params = self._prepare_begin_mcp_reauth(user_id, server_name)
2181
+ return self._request_post_form(endpoint, params)
2182
+
2069
2183
  def list_users(
2070
2184
  self,
2071
2185
  status: Optional[str] = None,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "assistant_runtime_sdk"
3
- version = "1.7.1"
3
+ version = "1.9.0"
4
4
  description = "Python SDK for FAC Cloud - the Assistant Runtime AI assistant backend"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0"}