wcgw 5.0.2__py3-none-any.whl → 5.1.0__py3-none-any.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.

Potentially problematic release.


This version of wcgw might be problematic. Click here for more details.

mcp_wcgw/shared/memory.py DELETED
@@ -1,87 +0,0 @@
1
- """
2
- In-memory transports
3
- """
4
-
5
- from contextlib import asynccontextmanager
6
- from datetime import timedelta
7
- from typing import AsyncGenerator
8
-
9
- import anyio
10
- from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
11
-
12
- from mcp_wcgw.client.session import ClientSession
13
- from mcp_wcgw.server import Server
14
- from mcp_wcgw.types import JSONRPCMessage
15
-
16
- MessageStream = tuple[
17
- MemoryObjectReceiveStream[JSONRPCMessage | Exception],
18
- MemoryObjectSendStream[JSONRPCMessage],
19
- ]
20
-
21
-
22
- @asynccontextmanager
23
- async def create_client_server_memory_streams() -> (
24
- AsyncGenerator[tuple[MessageStream, MessageStream], None]
25
- ):
26
- """
27
- Creates a pair of bidirectional memory streams for client-server communication.
28
-
29
- Returns:
30
- A tuple of (client_streams, server_streams) where each is a tuple of
31
- (read_stream, write_stream)
32
- """
33
- # Create streams for both directions
34
- server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[
35
- JSONRPCMessage | Exception
36
- ](1)
37
- client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[
38
- JSONRPCMessage | Exception
39
- ](1)
40
-
41
- client_streams = (server_to_client_receive, client_to_server_send)
42
- server_streams = (client_to_server_receive, server_to_client_send)
43
-
44
- async with (
45
- server_to_client_receive,
46
- client_to_server_send,
47
- client_to_server_receive,
48
- server_to_client_send,
49
- ):
50
- yield client_streams, server_streams
51
-
52
-
53
- @asynccontextmanager
54
- async def create_connected_server_and_client_session(
55
- server: Server,
56
- read_timeout_seconds: timedelta | None = None,
57
- raise_exceptions: bool = False,
58
- ) -> AsyncGenerator[ClientSession, None]:
59
- """Creates a ClientSession that is connected to a running MCP server."""
60
- async with create_client_server_memory_streams() as (
61
- client_streams,
62
- server_streams,
63
- ):
64
- client_read, client_write = client_streams
65
- server_read, server_write = server_streams
66
-
67
- # Create a cancel scope for the server task
68
- async with anyio.create_task_group() as tg:
69
- tg.start_soon(
70
- lambda: server.run(
71
- server_read,
72
- server_write,
73
- server.create_initialization_options(),
74
- raise_exceptions=raise_exceptions,
75
- )
76
- )
77
-
78
- try:
79
- async with ClientSession(
80
- read_stream=client_read,
81
- write_stream=client_write,
82
- read_timeout_seconds=read_timeout_seconds,
83
- ) as client_session:
84
- await client_session.initialize()
85
- yield client_session
86
- finally:
87
- tg.cancel_scope.cancel()
@@ -1,40 +0,0 @@
1
- from contextlib import contextmanager
2
- from dataclasses import dataclass, field
3
-
4
- from pydantic import BaseModel
5
-
6
- from mcp_wcgw.shared.context import RequestContext
7
- from mcp_wcgw.shared.session import BaseSession
8
- from mcp_wcgw.types import ProgressToken
9
-
10
-
11
- class Progress(BaseModel):
12
- progress: float
13
- total: float | None
14
-
15
-
16
- @dataclass
17
- class ProgressContext:
18
- session: BaseSession
19
- progress_token: ProgressToken
20
- total: float | None
21
- current: float = field(default=0.0, init=False)
22
-
23
- async def progress(self, amount: float) -> None:
24
- self.current += amount
25
-
26
- await self.session.send_progress_notification(
27
- self.progress_token, self.current, total=self.total
28
- )
29
-
30
-
31
- @contextmanager
32
- def progress(ctx: RequestContext, total: float | None = None):
33
- if ctx.meta is None or ctx.meta.progressToken is None:
34
- raise ValueError("No progress token provided")
35
-
36
- progress_ctx = ProgressContext(ctx.session, ctx.meta.progressToken, total)
37
- try:
38
- yield progress_ctx
39
- finally:
40
- pass
@@ -1,288 +0,0 @@
1
- from contextlib import AbstractAsyncContextManager
2
- from datetime import timedelta
3
- from typing import Generic, TypeVar
4
-
5
- import anyio
6
- import anyio.lowlevel
7
- import httpx
8
- from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
9
- from pydantic import BaseModel
10
-
11
- from mcp_wcgw.shared.exceptions import McpError
12
- from mcp_wcgw.types import (
13
- ClientNotification,
14
- ClientRequest,
15
- ClientResult,
16
- ErrorData,
17
- JSONRPCError,
18
- JSONRPCMessage,
19
- JSONRPCNotification,
20
- JSONRPCRequest,
21
- JSONRPCResponse,
22
- RequestParams,
23
- ServerNotification,
24
- ServerRequest,
25
- ServerResult,
26
- )
27
-
28
- SendRequestT = TypeVar("SendRequestT", ClientRequest, ServerRequest)
29
- SendResultT = TypeVar("SendResultT", ClientResult, ServerResult)
30
- SendNotificationT = TypeVar("SendNotificationT", ClientNotification, ServerNotification)
31
- ReceiveRequestT = TypeVar("ReceiveRequestT", ClientRequest, ServerRequest)
32
- ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel)
33
- ReceiveNotificationT = TypeVar(
34
- "ReceiveNotificationT", ClientNotification, ServerNotification
35
- )
36
-
37
- RequestId = str | int
38
-
39
-
40
- class RequestResponder(Generic[ReceiveRequestT, SendResultT]):
41
- def __init__(
42
- self,
43
- request_id: RequestId,
44
- request_meta: RequestParams.Meta | None,
45
- request: ReceiveRequestT,
46
- session: "BaseSession",
47
- ) -> None:
48
- self.request_id = request_id
49
- self.request_meta = request_meta
50
- self.request = request
51
- self._session = session
52
- self._responded = False
53
-
54
- async def respond(self, response: SendResultT | ErrorData) -> None:
55
- assert not self._responded, "Request already responded to"
56
- self._responded = True
57
-
58
- await self._session._send_response(
59
- request_id=self.request_id, response=response
60
- )
61
-
62
-
63
- class BaseSession(
64
- AbstractAsyncContextManager,
65
- Generic[
66
- SendRequestT,
67
- SendNotificationT,
68
- SendResultT,
69
- ReceiveRequestT,
70
- ReceiveNotificationT,
71
- ],
72
- ):
73
- """
74
- Implements an MCP "session" on top of read/write streams, including features
75
- like request/response linking, notifications, and progress.
76
-
77
- This class is an async context manager that automatically starts processing
78
- messages when entered.
79
- """
80
-
81
- _response_streams: dict[
82
- RequestId, MemoryObjectSendStream[JSONRPCResponse | JSONRPCError]
83
- ]
84
- _request_id: int
85
-
86
- def __init__(
87
- self,
88
- read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
89
- write_stream: MemoryObjectSendStream[JSONRPCMessage],
90
- receive_request_type: type[ReceiveRequestT],
91
- receive_notification_type: type[ReceiveNotificationT],
92
- # If none, reading will never time out
93
- read_timeout_seconds: timedelta | None = None,
94
- ) -> None:
95
- self._read_stream = read_stream
96
- self._write_stream = write_stream
97
- self._response_streams = {}
98
- self._request_id = 0
99
- self._receive_request_type = receive_request_type
100
- self._receive_notification_type = receive_notification_type
101
- self._read_timeout_seconds = read_timeout_seconds
102
-
103
- self._incoming_message_stream_writer, self._incoming_message_stream_reader = (
104
- anyio.create_memory_object_stream[
105
- RequestResponder[ReceiveRequestT, SendResultT]
106
- | ReceiveNotificationT
107
- | Exception
108
- ]()
109
- )
110
-
111
- async def __aenter__(self):
112
- self._task_group = anyio.create_task_group()
113
- await self._task_group.__aenter__()
114
- self._task_group.start_soon(self._receive_loop)
115
- return self
116
-
117
- async def __aexit__(self, exc_type, exc_val, exc_tb):
118
- # Using BaseSession as a context manager should not block on exit (this
119
- # would be very surprising behavior), so make sure to cancel the tasks
120
- # in the task group.
121
- self._task_group.cancel_scope.cancel()
122
- return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
123
-
124
- async def send_request(
125
- self,
126
- request: SendRequestT,
127
- result_type: type[ReceiveResultT],
128
- ) -> ReceiveResultT:
129
- """
130
- Sends a request and wait for a response. Raises an McpError if the
131
- response contains an error.
132
-
133
- Do not use this method to emit notifications! Use send_notification()
134
- instead.
135
- """
136
-
137
- request_id = self._request_id
138
- self._request_id = request_id + 1
139
-
140
- response_stream, response_stream_reader = anyio.create_memory_object_stream[
141
- JSONRPCResponse | JSONRPCError
142
- ](1)
143
- self._response_streams[request_id] = response_stream
144
-
145
- jsonrpc_request = JSONRPCRequest(
146
- jsonrpc="2.0",
147
- id=request_id,
148
- **request.model_dump(by_alias=True, mode="json", exclude_none=True),
149
- )
150
-
151
- # TODO: Support progress callbacks
152
-
153
- await self._write_stream.send(JSONRPCMessage(jsonrpc_request))
154
-
155
- try:
156
- with anyio.fail_after(
157
- None
158
- if self._read_timeout_seconds is None
159
- else self._read_timeout_seconds.total_seconds()
160
- ):
161
- response_or_error = await response_stream_reader.receive()
162
- except TimeoutError:
163
- raise McpError(
164
- ErrorData(
165
- code=httpx.codes.REQUEST_TIMEOUT,
166
- message=(
167
- f"Timed out while waiting for response to "
168
- f"{request.__class__.__name__}. Waited "
169
- f"{self._read_timeout_seconds} seconds."
170
- ),
171
- )
172
- )
173
-
174
- if isinstance(response_or_error, JSONRPCError):
175
- raise McpError(response_or_error.error)
176
- else:
177
- return result_type.model_validate(response_or_error.result)
178
-
179
- async def send_notification(self, notification: SendNotificationT) -> None:
180
- """
181
- Emits a notification, which is a one-way message that does not expect
182
- a response.
183
- """
184
- jsonrpc_notification = JSONRPCNotification(
185
- jsonrpc="2.0",
186
- **notification.model_dump(by_alias=True, mode="json", exclude_none=True),
187
- )
188
-
189
- await self._write_stream.send(JSONRPCMessage(jsonrpc_notification))
190
-
191
- async def _send_response(
192
- self, request_id: RequestId, response: SendResultT | ErrorData
193
- ) -> None:
194
- if isinstance(response, ErrorData):
195
- jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response)
196
- await self._write_stream.send(JSONRPCMessage(jsonrpc_error))
197
- else:
198
- jsonrpc_response = JSONRPCResponse(
199
- jsonrpc="2.0",
200
- id=request_id,
201
- result=response.model_dump(
202
- by_alias=True, mode="json", exclude_none=True
203
- ),
204
- )
205
- await self._write_stream.send(JSONRPCMessage(jsonrpc_response))
206
-
207
- async def _receive_loop(self) -> None:
208
- async with (
209
- self._read_stream,
210
- self._write_stream,
211
- self._incoming_message_stream_writer,
212
- ):
213
- async for message in self._read_stream:
214
- if isinstance(message, Exception):
215
- await self._incoming_message_stream_writer.send(message)
216
- elif isinstance(message.root, JSONRPCRequest):
217
- validated_request = self._receive_request_type.model_validate(
218
- message.root.model_dump(
219
- by_alias=True, mode="json", exclude_none=True
220
- )
221
- )
222
- responder = RequestResponder(
223
- request_id=message.root.id,
224
- request_meta=validated_request.root.params._meta
225
- if validated_request.root.params
226
- else None,
227
- request=validated_request,
228
- session=self,
229
- )
230
-
231
- await self._received_request(responder)
232
- if not responder._responded:
233
- await self._incoming_message_stream_writer.send(responder)
234
- elif isinstance(message.root, JSONRPCNotification):
235
- notification = self._receive_notification_type.model_validate(
236
- message.root.model_dump(
237
- by_alias=True, mode="json", exclude_none=True
238
- )
239
- )
240
-
241
- await self._received_notification(notification)
242
- await self._incoming_message_stream_writer.send(notification)
243
- else: # Response or error
244
- stream = self._response_streams.pop(message.root.id, None)
245
- if stream:
246
- await stream.send(message.root)
247
- else:
248
- await self._incoming_message_stream_writer.send(
249
- RuntimeError(
250
- "Received response with an unknown "
251
- f"request ID: {message}"
252
- )
253
- )
254
-
255
- async def _received_request(
256
- self, responder: RequestResponder[ReceiveRequestT, SendResultT]
257
- ) -> None:
258
- """
259
- Can be overridden by subclasses to handle a request without needing to
260
- listen on the message stream.
261
-
262
- If the request is responded to within this method, it will not be
263
- forwarded on to the message stream.
264
- """
265
-
266
- async def _received_notification(self, notification: ReceiveNotificationT) -> None:
267
- """
268
- Can be overridden by subclasses to handle a notification without needing
269
- to listen on the message stream.
270
- """
271
-
272
- async def send_progress_notification(
273
- self, progress_token: str | int, progress: float, total: float | None = None
274
- ) -> None:
275
- """
276
- Sends a progress notification for a request that is currently being
277
- processed.
278
- """
279
-
280
- @property
281
- def incoming_messages(
282
- self,
283
- ) -> MemoryObjectReceiveStream[
284
- RequestResponder[ReceiveRequestT, SendResultT]
285
- | ReceiveNotificationT
286
- | Exception
287
- ]:
288
- return self._incoming_message_stream_reader
@@ -1,3 +0,0 @@
1
- from mcp_wcgw.types import LATEST_PROTOCOL_VERSION
2
-
3
- SUPPORTED_PROTOCOL_VERSIONS = [1, LATEST_PROTOCOL_VERSION]