ms-tau-sdk 2.0.12.dev53__tar.gz → 2.0.13.dev54__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.
Files changed (86) hide show
  1. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/CHANGELOG.md +12 -0
  2. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/PKG-INFO +2 -2
  3. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/pyproject.toml +2 -2
  4. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/mcp.py +97 -65
  5. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/tools/mainsequence_mcp.py +1 -1
  6. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/tools/mcp_applications.py +22 -17
  7. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/tools/mcp_connection.py +14 -12
  8. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/.gitignore +0 -0
  9. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/README.md +0 -0
  10. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  11. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  12. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  13. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  14. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  15. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  16. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  17. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  18. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  19. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  20. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  21. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  22. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  23. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/__init__.py +0 -0
  24. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
  25. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
  26. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
  27. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  28. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/agent_skills/tau_security_and_access/SKILL.md +0 -0
  29. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/__init__.py +0 -0
  30. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/a2a.py +0 -0
  31. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/chat.py +0 -0
  32. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/conversations.py +0 -0
  33. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/dependencies.py +0 -0
  34. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/health.py +0 -0
  35. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/inspection.py +0 -0
  36. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/local_chat.py +0 -0
  37. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/models.py +0 -0
  38. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/request_identity.py +0 -0
  39. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/api/sessions.py +0 -0
  40. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/app.py +0 -0
  41. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/application.py +0 -0
  42. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/__init__.py +0 -0
  43. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/assertions.py +0 -0
  44. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/auth.py +0 -0
  45. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/client.py +0 -0
  46. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/local.py +0 -0
  47. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/models.py +0 -0
  48. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/backend/routes.py +0 -0
  49. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/cli.py +0 -0
  50. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/errors.py +0 -0
  51. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/logging.py +0 -0
  52. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  53. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  54. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  55. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  56. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  57. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
  58. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  59. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/providers/__init__.py +0 -0
  60. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/providers/definitions.py +0 -0
  61. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/providers/factory.py +0 -0
  62. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  63. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  64. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/resources/__init__.py +0 -0
  65. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/resources/loader.py +0 -0
  66. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  67. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  68. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  69. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/events.py +0 -0
  70. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  71. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/failures.py +0 -0
  72. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
  73. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/manager.py +0 -0
  74. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/observability.py +0 -0
  75. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  76. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/requester.py +0 -0
  77. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/session.py +0 -0
  78. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  79. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  80. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  81. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/sessions/storage.py +0 -0
  82. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/settings.py +0 -0
  83. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/skills.py +0 -0
  84. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/tools/__init__.py +0 -0
  85. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/tools/skill_read.py +0 -0
  86. {ms_tau_sdk-2.0.12.dev53 → ms_tau_sdk-2.0.13.dev54}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - **ms-tau-sdk installs the MCP Python SDK 2.x.** The `mcp` dependency moves from `mcp>=1.27,<2`
6
+ to `mcp>=2.3,<3`, which brings `httpx2`, the HTTP client MCP 2.x uses. A repository that runs a
7
+ Harness Agent and also serves an application MCP with `mainsequence[mcp]`, which requires MCP 2.x
8
+ for the MCP Skills extension, could not lock both packages; it now can. Agent behaviour does not
9
+ change: platform and application MCP calls keep their time limits, cancellation, the one token
10
+ renewal on 401, and the same failure texts and details, including the refused, server-error and
11
+ timeout statuses that MCP 2.x no longer reports by itself. A project that pins `mcp<2` itself,
12
+ or uses the MCP 1.x API in its own code (`FastMCP`, `McpError`, camelCase fields), must move to
13
+ MCP 2.x. The time-limits guide shows `MCPServer`, MCP 2.x's name for `FastMCP`.
14
+
3
15
  ## 2.0.12 — 2026-10-11
4
16
 
5
17
  - **A direct Message that runs out of time gets a typed answer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 2.0.12.dev53
3
+ Version: 2.0.13.dev54
4
4
  Summary: Workspace-bound Tau application primitives for Main Sequence projects
5
5
  Project-URL: Changelog, https://github.com/mainsequence-sdk/ms-tau-sdk/blob/development/CHANGELOG.md
6
6
  Project-URL: Documentation, https://github.com/mainsequence-sdk/ms-tau-sdk/tree/development/docs
@@ -18,7 +18,7 @@ Requires-Dist: cryptography>=42
18
18
  Requires-Dist: fastapi>=0.116
19
19
  Requires-Dist: httpx>=0.28
20
20
  Requires-Dist: jsonschema<5,>=4.25
21
- Requires-Dist: mcp<2,>=1.27
21
+ Requires-Dist: mcp<3,>=2.3
22
22
  Requires-Dist: pydantic-settings>=2.10
23
23
  Requires-Dist: pydantic>=2.11
24
24
  Requires-Dist: pyjwt[crypto]<3,>=2.10.1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "2.0.12.dev53"
7
+ version = "2.0.13.dev54"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "fastapi>=0.116",
23
23
  "httpx>=0.28",
24
24
  "jsonschema>=4.25,<5",
25
- "mcp>=1.27,<2",
25
+ "mcp>=2.3,<3",
26
26
  "pydantic>=2.11",
27
27
  "pydantic-settings>=2.10",
28
28
  "pyjwt[crypto]>=2.10.1,<3",
@@ -5,20 +5,22 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import contextvars
7
7
  import math
8
- from collections.abc import AsyncGenerator, AsyncIterator, Callable
8
+ from collections.abc import AsyncGenerator, AsyncIterator
9
9
  from contextlib import asynccontextmanager
10
10
  from dataclasses import dataclass
11
- from datetime import timedelta
11
+ from types import TracebackType
12
12
  from typing import Any, Literal, Self, cast
13
13
 
14
14
  import anyio
15
- import httpx
16
- from anyio.abc import ObjectSendStream
17
- from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
15
+ import httpx2
18
16
  from mcp import ClientSession, types
19
17
  from mcp.client.streamable_http import RequestContext, StreamableHTTPTransport
18
+ from mcp.shared._context_streams import (
19
+ ContextReceiveStream,
20
+ ContextSendStream,
21
+ create_context_streams,
22
+ )
20
23
  from mcp.shared.message import SessionMessage
21
- from pydantic import AnyUrl
22
24
  from structlog.contextvars import bound_contextvars, get_contextvars
23
25
 
24
26
  from ms_tau_sdk.settings import TauSDKSettings
@@ -36,7 +38,18 @@ def _actionable_cleanup_error(error: BaseException) -> Exception | None:
36
38
  return error if isinstance(error, Exception) else None
37
39
 
38
40
 
39
- class _RuntimeCredentialHTTPXAuth(httpx.Auth):
41
+ def raise_for_renewed_401(response: httpx2.Response) -> None:
42
+ """End the call when a request still gets 401 after its credential was renewed.
43
+
44
+ MCP 2.x turns an HTTP error into a JSON-RPC error without its status. Raising here keeps the
45
+ status, as MCP 1.x's transport did, so callers can still say the call was refused.
46
+ """
47
+
48
+ if response.status_code == 401:
49
+ response.raise_for_status()
50
+
51
+
52
+ class _RuntimeCredentialHTTPXAuth(httpx2.Auth):
40
53
  requires_request_body = True
41
54
 
42
55
  def __init__(self, runtime_auth: BackendAuth) -> None:
@@ -44,13 +57,13 @@ class _RuntimeCredentialHTTPXAuth(httpx.Auth):
44
57
 
45
58
  async def async_auth_flow(
46
59
  self,
47
- request: httpx.Request,
48
- ) -> AsyncGenerator[httpx.Request, httpx.Response]:
60
+ request: httpx2.Request,
61
+ ) -> AsyncGenerator[httpx2.Request, httpx2.Response]:
49
62
  request.headers.update(await self.runtime_auth.headers())
50
63
  response = yield request
51
64
  if response.status_code == 401:
52
65
  request.headers.update(await self.runtime_auth.headers(force=True))
53
- yield request
66
+ raise_for_renewed_401((yield request))
54
67
 
55
68
 
56
69
  @dataclass
@@ -75,7 +88,7 @@ _tool_call: contextvars.ContextVar[_ToolCall | None] = contextvars.ContextVar(
75
88
  )
76
89
 
77
90
 
78
- class _ToolCallWriteStream(ObjectSendStream[SessionMessage]):
91
+ class _ToolCallWriteStream:
79
92
  """Associate the session's request ID with its call before the transport sends it.
80
93
 
81
94
  MCP's HTTP writer runs in another task, so the calling task's context does not reach its
@@ -84,14 +97,14 @@ class _ToolCallWriteStream(ObjectSendStream[SessionMessage]):
84
97
 
85
98
  def __init__(
86
99
  self,
87
- stream: MemoryObjectSendStream[SessionMessage],
100
+ stream: ContextSendStream[SessionMessage],
88
101
  calls: dict[types.RequestId, _ToolCall],
89
102
  ) -> None:
90
103
  self._stream = stream
91
104
  self._calls = calls
92
105
 
93
- async def send(self, item: SessionMessage) -> None:
94
- message = item.message.root
106
+ async def send(self, item: SessionMessage, /) -> None:
107
+ message = item.message
95
108
  call = _tool_call.get()
96
109
  if (
97
110
  call is not None
@@ -105,6 +118,18 @@ class _ToolCallWriteStream(ObjectSendStream[SessionMessage]):
105
118
  async def aclose(self) -> None:
106
119
  await self._stream.aclose()
107
120
 
121
+ async def __aenter__(self) -> Self:
122
+ return self
123
+
124
+ async def __aexit__(
125
+ self,
126
+ exc_type: type[BaseException] | None,
127
+ exc_val: BaseException | None,
128
+ exc_tb: TracebackType | None,
129
+ ) -> bool | None:
130
+ await self.aclose()
131
+ return None
132
+
108
133
 
109
134
  class _ToolCallTransport(StreamableHTTPTransport):
110
135
  """Give each call's POST a cancellation scope inside MCP's transport task.
@@ -118,7 +143,7 @@ class _ToolCallTransport(StreamableHTTPTransport):
118
143
  self._calls = calls
119
144
 
120
145
  async def _handle_post_request(self, ctx: RequestContext) -> None:
121
- message = ctx.session_message.message.root
146
+ message = ctx.session_message.message
122
147
  call = (
123
148
  self._calls.get(message.id)
124
149
  if isinstance(message, types.JSONRPCRequest) and message.method == "tools/call"
@@ -143,14 +168,13 @@ class _ToolCallTransport(StreamableHTTPTransport):
143
168
  async def streamable_http_client(
144
169
  url: str,
145
170
  *,
146
- http_client: httpx.AsyncClient,
171
+ http_client: httpx2.AsyncClient,
147
172
  calls: dict[types.RequestId, _ToolCall],
148
173
  terminate_on_close: bool = False,
149
174
  ) -> AsyncIterator[
150
175
  tuple[
151
- MemoryObjectReceiveStream[SessionMessage | Exception],
152
- MemoryObjectSendStream[SessionMessage],
153
- Callable[[], str | None],
176
+ ContextReceiveStream[SessionMessage | Exception],
177
+ ContextSendStream[SessionMessage],
154
178
  ]
155
179
  ]:
156
180
  """Use MCP's HTTP readers/writer with call-scoped POST cancellation.
@@ -158,33 +182,35 @@ async def streamable_http_client(
158
182
  The upstream factory constructs its transport internally without an injection point.
159
183
  Preserve its stream/task-group lifecycle while selecting our transport specialization.
160
184
  """
161
- read_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0)
162
- write_stream, write_reader = anyio.create_memory_object_stream[SessionMessage](0)
185
+ read_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
186
+ write_stream, write_reader = create_context_streams[SessionMessage](0)
163
187
  transport = _ToolCallTransport(url, calls)
164
- async with anyio.create_task_group() as task_group:
188
+ async with (
189
+ read_writer,
190
+ read_stream,
191
+ write_stream,
192
+ write_reader,
193
+ anyio.create_task_group() as task_group,
194
+ ):
195
+
196
+ def start_get_stream() -> None:
197
+ task_group.start_soon(transport.handle_get_stream, http_client, read_writer)
198
+
199
+ task_group.start_soon(
200
+ transport.post_writer,
201
+ http_client,
202
+ write_reader,
203
+ read_writer,
204
+ write_stream,
205
+ start_get_stream,
206
+ task_group,
207
+ )
165
208
  try:
166
-
167
- def start_get_stream() -> None:
168
- task_group.start_soon(transport.handle_get_stream, http_client, read_writer)
169
-
170
- task_group.start_soon(
171
- transport.post_writer,
172
- http_client,
173
- write_reader,
174
- read_writer,
175
- write_stream,
176
- start_get_stream,
177
- task_group,
178
- )
179
- try:
180
- yield read_stream, write_stream, transport.get_session_id
181
- finally:
182
- if terminate_on_close:
183
- await transport.terminate_session(http_client)
184
- task_group.cancel_scope.cancel()
209
+ yield read_stream, write_stream
185
210
  finally:
186
- await read_writer.aclose()
187
- await write_stream.aclose()
211
+ if transport.session_id and terminate_on_close:
212
+ await transport.terminate_session(http_client)
213
+ task_group.cancel_scope.cancel()
188
214
 
189
215
 
190
216
  def _validate_response_timeout(seconds: float) -> None:
@@ -241,8 +267,8 @@ class MCPConnectionClient:
241
267
  name: str,
242
268
  display_name: str,
243
269
  url: str,
244
- auth: httpx.Auth | None,
245
- timeout: httpx.Timeout,
270
+ auth: httpx2.Auth | None,
271
+ timeout: httpx2.Timeout,
246
272
  read_timeout_seconds: float,
247
273
  read_concurrency: int,
248
274
  ) -> None:
@@ -300,25 +326,24 @@ class MCPConnectionClient:
300
326
  async def _run(self, initial_log_context: dict[str, object]) -> None:
301
327
  failure: Exception | None = None
302
328
  try:
303
- async with httpx.AsyncClient(
329
+ async with httpx2.AsyncClient(
304
330
  timeout=self._http_timeout,
305
331
  auth=self._http_auth,
306
- event_hooks={"request": [self._prepare_tool_request]},
332
+ event_hooks={
333
+ "request": [self._prepare_tool_request],
334
+ "response": [self._raise_for_error_status],
335
+ },
307
336
  ) as http_client:
308
337
  async with streamable_http_client(
309
338
  self.url,
310
339
  http_client=http_client,
311
340
  calls=self._http_calls,
312
341
  terminate_on_close=False,
313
- ) as (read_stream, write_stream, _get_session_id):
342
+ ) as (read_stream, write_stream):
314
343
  async with ClientSession(
315
344
  read_stream,
316
- # MCP annotates this as a memory stream, but uses only send/aclose.
317
- cast(
318
- MemoryObjectSendStream[SessionMessage],
319
- _ToolCallWriteStream(write_stream, self._http_calls),
320
- ),
321
- read_timeout_seconds=timedelta(seconds=self._read_timeout_seconds),
345
+ _ToolCallWriteStream(write_stream, self._http_calls),
346
+ read_timeout_seconds=self._read_timeout_seconds,
322
347
  ) as session:
323
348
  with bound_contextvars(**initial_log_context):
324
349
  initialized = await session.initialize()
@@ -327,8 +352,8 @@ class MCPConnectionClient:
327
352
  tool.name
328
353
  for tool in self.tools
329
354
  if tool.annotations is not None
330
- and tool.annotations.readOnlyHint is True
331
- and tool.annotations.idempotentHint is True
355
+ and tool.annotations.read_only_hint is True
356
+ and tool.annotations.idempotent_hint is True
332
357
  )
333
358
  if self._ready is not None and not self._ready.done():
334
359
  self._ready.set_result(None)
@@ -349,10 +374,10 @@ class MCPConnectionClient:
349
374
  )
350
375
  self._http_calls.clear()
351
376
 
352
- async def _prepare_tool_request(self, request: httpx.Request) -> None:
377
+ async def _prepare_tool_request(self, request: httpx2.Request) -> None:
353
378
  if request.method != "POST":
354
379
  return
355
- message = types.JSONRPCMessage.model_validate_json(request.content).root
380
+ message = types.jsonrpc_message_adapter.validate_json(request.content)
356
381
  if (
357
382
  isinstance(message, types.JSONRPCRequest)
358
383
  and message.method == "tools/call"
@@ -362,6 +387,13 @@ class MCPConnectionClient:
362
387
  # connect/write/pool limits, without mutating the shared HTTP client defaults.
363
388
  request.extensions["timeout"] = {**request.extensions["timeout"], "read": None}
364
389
 
390
+ async def _raise_for_error_status(self, response: httpx2.Response) -> None:
391
+ # MCP 2.x answers an HTTP error on a message POST with a JSON-RPC error that drops the
392
+ # status. Raise it, as MCP 1.x did, so a failure still says refused, server error or
393
+ # timeout. 401 is left to the credential's renewal, and 404 to MCP, as 1.x did.
394
+ if response.request.method == "POST" and response.status_code not in {401, 404}:
395
+ response.raise_for_status()
396
+
365
397
  async def _read_server_catalog(
366
398
  self,
367
399
  session: ClientSession,
@@ -396,21 +428,21 @@ class MCPConnectionClient:
396
428
  call = _ToolCall(cancel_scope)
397
429
  token = _tool_call.set(call)
398
430
  command.result.add_done_callback(call.cancel_if_abandoned)
431
+ # MCP types `_meta` as a TypedDict; the call's own keys pass through.
432
+ meta = cast(types.RequestParamsMeta | None, command.meta)
399
433
  try:
400
434
  if command.timeout_seconds is None:
401
435
  tool_result = await session.call_tool(
402
436
  command.name,
403
437
  command.arguments,
404
- meta=command.meta,
438
+ meta=meta,
405
439
  )
406
440
  else:
407
441
  tool_result = await session.call_tool(
408
442
  command.name,
409
443
  command.arguments,
410
- read_timeout_seconds=timedelta(
411
- seconds=command.timeout_seconds
412
- ),
413
- meta=command.meta,
444
+ read_timeout_seconds=command.timeout_seconds,
445
+ meta=meta,
414
446
  )
415
447
  if not command.result.done():
416
448
  command.result.set_result(tool_result)
@@ -419,7 +451,7 @@ class MCPConnectionClient:
419
451
  _tool_call.reset(token)
420
452
  call.finish()
421
453
  else:
422
- resource_result = await session.read_resource(AnyUrl(command.uri))
454
+ resource_result = await session.read_resource(command.uri)
423
455
  if not command.result.done():
424
456
  command.result.set_result(resource_result)
425
457
  except Exception as error:
@@ -557,7 +589,7 @@ class MainSequenceMCPClient(MCPConnectionClient):
557
589
  display_name="Main Sequence",
558
590
  url=f"{settings.backend_url.rstrip('/')}/mcp",
559
591
  auth=_RuntimeCredentialHTTPXAuth(auth),
560
- timeout=httpx.Timeout(
592
+ timeout=httpx2.Timeout(
561
593
  connect=settings.backend_connect_timeout_seconds,
562
594
  read=settings.backend_read_timeout_seconds,
563
595
  write=settings.backend_write_timeout_seconds,
@@ -43,7 +43,7 @@ def _tau_tool_input_schema(
43
43
  *,
44
44
  local_user_semantics: bool = False,
45
45
  ) -> Mapping[str, JSONValue]:
46
- schema = deepcopy(tool.inputSchema)
46
+ schema = deepcopy(tool.input_schema)
47
47
  if tool.name == A2A_SEND_TOOL:
48
48
  properties = schema.setdefault("properties", {})
49
49
  if isinstance(properties, dict):
@@ -22,8 +22,9 @@ from collections.abc import AsyncGenerator, Mapping, Sequence
22
22
  from typing import Protocol
23
23
 
24
24
  import httpx
25
+ import httpx2
25
26
  import structlog
26
- from mcp import McpError, types
27
+ from mcp import MCPError, types
27
28
  from tau_agent.messages import TextContent
28
29
  from tau_agent.tools import (
29
30
  AgentTool,
@@ -33,7 +34,7 @@ from tau_agent.tools import (
33
34
  )
34
35
  from tau_agent.types import JSONValue
35
36
 
36
- from ms_tau_sdk.backend.mcp import MCPConnectionClient
37
+ from ms_tau_sdk.backend.mcp import MCPConnectionClient, raise_for_renewed_401
37
38
  from ms_tau_sdk.backend.models import MCPApplication
38
39
  from ms_tau_sdk.errors import (
39
40
  ApplicationReadinessTimeoutError,
@@ -63,7 +64,7 @@ class ApplicationConnector(Protocol):
63
64
  ) -> MCPConnectionClient: ...
64
65
 
65
66
 
66
- class _ApplicationTokenAuth(httpx.Auth):
67
+ class _ApplicationTokenAuth(httpx2.Auth):
67
68
  """Send the turn's application token; obtain a new one once when the application says 401."""
68
69
 
69
70
  def __init__(self, access: _TurnApplicationAccess) -> None:
@@ -72,14 +73,14 @@ class _ApplicationTokenAuth(httpx.Auth):
72
73
 
73
74
  async def async_auth_flow(
74
75
  self,
75
- request: httpx.Request,
76
- ) -> AsyncGenerator[httpx.Request, httpx.Response]:
76
+ request: httpx2.Request,
77
+ ) -> AsyncGenerator[httpx2.Request, httpx2.Response]:
77
78
  request.headers["Authorization"] = f"Bearer {self._token}"
78
79
  response = yield request
79
80
  if response.status_code == 401:
80
81
  self._token = await self._renew()
81
82
  request.headers["Authorization"] = f"Bearer {self._token}"
82
- yield request
83
+ raise_for_renewed_401((yield request))
83
84
 
84
85
 
85
86
  def application_connector(settings: TauSDKSettings) -> ApplicationConnector:
@@ -100,7 +101,7 @@ def application_connector(settings: TauSDKSettings) -> ApplicationConnector:
100
101
  auth=_ApplicationTokenAuth(access),
101
102
  # The connection applies the tool's response deadline to its call; these HTTP limits
102
103
  # also bound initialization and catalog requests.
103
- timeout=httpx.Timeout(
104
+ timeout=httpx2.Timeout(
104
105
  connect=settings.backend_connect_timeout_seconds,
105
106
  read=settings.backend_read_timeout_seconds,
106
107
  write=settings.backend_write_timeout_seconds,
@@ -114,10 +115,11 @@ def application_connector(settings: TauSDKSettings) -> ApplicationConnector:
114
115
 
115
116
 
116
117
  def _timed_out(error: BaseException) -> bool:
117
- if isinstance(error, McpError):
118
- return error.error.code == httpx.codes.REQUEST_TIMEOUT
118
+ if isinstance(error, MCPError):
119
+ return error.code == types.REQUEST_TIMEOUT
119
120
  return isinstance(error, TimeoutError) or (
120
- isinstance(error, httpx.TimeoutException) and not isinstance(error, httpx.ConnectTimeout)
121
+ isinstance(error, httpx.TimeoutException | httpx2.TimeoutException)
122
+ and not isinstance(error, httpx.ConnectTimeout | httpx2.ConnectTimeout)
121
123
  )
122
124
 
123
125
 
@@ -175,7 +177,7 @@ def _failure(
175
177
  f"{subject} did not answer within {timeout_seconds:g} seconds.",
176
178
  {"failure": "timeout", "timeout_seconds": timeout_seconds},
177
179
  )
178
- if isinstance(error, httpx.HTTPStatusError):
180
+ if isinstance(error, httpx.HTTPStatusError | httpx2.HTTPStatusError):
179
181
  status = error.response.status_code
180
182
  if status in {408, 504}:
181
183
  text = f"The {name} application or its gateway reported a timeout ({status})."
@@ -188,7 +190,10 @@ def _failure(
188
190
  return text, {"failure": "refused", "status": status}
189
191
  text = f"The {name} application answered with an error ({status})."
190
192
  return text, {"failure": "error_status", "status": status}
191
- if isinstance(error, httpx.ConnectError | httpx.ConnectTimeout):
193
+ if isinstance(
194
+ error,
195
+ httpx.ConnectError | httpx.ConnectTimeout | httpx2.ConnectError | httpx2.ConnectTimeout,
196
+ ):
192
197
  return f"The {name} application could not be reached.", {"failure": "unreachable"}
193
198
  return f"The {name} application could not complete this call.", {"failure": "failed"}
194
199
 
@@ -196,9 +201,9 @@ def _failure(
196
201
  def _retryable(error: BaseException) -> bool:
197
202
  """Whether opening the session failed in a way that a second attempt may not repeat."""
198
203
 
199
- if isinstance(error, httpx.HTTPStatusError):
204
+ if isinstance(error, httpx.HTTPStatusError | httpx2.HTTPStatusError):
200
205
  return error.response.status_code == 408 or error.response.status_code >= 500
201
- return isinstance(error, httpx.TransportError) or _timed_out(error)
206
+ return isinstance(error, httpx.TransportError | httpx2.TransportError) or _timed_out(error)
202
207
 
203
208
 
204
209
  def _error(name: str, text: str, **details: JSONValue) -> AgentToolResult:
@@ -209,11 +214,11 @@ def _error(name: str, text: str, **details: JSONValue) -> AgentToolResult:
209
214
 
210
215
 
211
216
  def _tool_entry(tool: types.Tool) -> dict[str, JSONValue]:
212
- entry: dict[str, JSONValue] = {"name": tool.name, "inputSchema": dict(tool.inputSchema)}
217
+ entry: dict[str, JSONValue] = {"name": tool.name, "inputSchema": dict(tool.input_schema)}
213
218
  if tool.description:
214
219
  entry["description"] = tool.description
215
- if tool.annotations is not None and tool.annotations.readOnlyHint is not None:
216
- entry["readOnly"] = tool.annotations.readOnlyHint
220
+ if tool.annotations is not None and tool.annotations.read_only_hint is not None:
221
+ entry["readOnly"] = tool.annotations.read_only_hint
217
222
  return entry
218
223
 
219
224
 
@@ -124,24 +124,26 @@ def mcp_tool_result(
124
124
  content.append(
125
125
  ImageContent(
126
126
  data=block.data,
127
- mime_type=block.mimeType,
127
+ mime_type=block.mime_type,
128
128
  )
129
129
  )
130
130
  else:
131
- content.append(TextContent(text=_json_text(block.model_dump(mode="json"))))
132
- if not content and result.structuredContent is not None:
133
- content.append(TextContent(text=_json_text(result.structuredContent)))
134
- if not content and result.isError:
131
+ content.append(
132
+ TextContent(text=_json_text(block.model_dump(by_alias=True, mode="json")))
133
+ )
134
+ if not content and result.structured_content is not None:
135
+ content.append(TextContent(text=_json_text(result.structured_content)))
136
+ if not content and result.is_error:
135
137
  content.append(TextContent(text=f"{display_name} MCP tool {canonical_name} failed."))
136
138
 
137
139
  details: dict[str, JSONValue] = {
138
140
  "mcp_tool": canonical_name,
139
- "is_error": result.isError,
141
+ "is_error": result.is_error,
140
142
  }
141
- if result.structuredContent is not None:
143
+ if result.structured_content is not None:
142
144
  details["structured_content"] = cast(
143
145
  JSONValue,
144
- result.structuredContent,
146
+ result.structured_content,
145
147
  )
146
148
  return AgentToolResult(content=content, details=details)
147
149
 
@@ -159,8 +161,8 @@ def _create_tool(
159
161
  execution_mode: Literal["sequential", "parallel"] = (
160
162
  "parallel"
161
163
  if annotations is not None
162
- and bool(getattr(annotations, "readOnlyHint", False))
163
- and bool(getattr(annotations, "idempotentHint", False))
164
+ and bool(getattr(annotations, "read_only_hint", False))
165
+ and bool(getattr(annotations, "idempotent_hint", False))
164
166
  else "sequential"
165
167
  )
166
168
  # A tool that declares its time limit is waited for that long; any other tool gets the
@@ -199,7 +201,7 @@ def _create_tool(
199
201
  parameters = (
200
202
  policy.input_schema(tool)
201
203
  if policy.input_schema is not None
202
- else cast(Mapping[str, JSONValue], deepcopy(tool.inputSchema))
204
+ else cast(Mapping[str, JSONValue], deepcopy(tool.input_schema))
203
205
  )
204
206
  return AgentTool(
205
207
  name=tau_name,
@@ -241,7 +243,7 @@ def _create_resource_tool(client: MCPConnection, policy: MCPToolPolicy) -> Agent
241
243
  else:
242
244
  content.append(
243
245
  TextContent(
244
- text=_json_text(item.model_dump(mode="json")),
246
+ text=_json_text(item.model_dump(by_alias=True, mode="json")),
245
247
  )
246
248
  )
247
249
  return AgentToolResult(