Skip to content

Commit a2f889b

Browse files
authored
Merge branch 'main' into main
2 parents 605a6d4 + 5441767 commit a2f889b

File tree

10 files changed

+560
-230
lines changed

10 files changed

+560
-230
lines changed

README.md

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -315,27 +315,42 @@ async def long_task(files: list[str], ctx: Context) -> str:
315315
Authentication can be used by servers that want to expose tools accessing protected resources.
316316

317317
`mcp.server.auth` implements an OAuth 2.0 server interface, which servers can use by
318-
providing an implementation of the `OAuthServerProvider` protocol.
318+
providing an implementation of the `OAuthAuthorizationServerProvider` protocol.
319319

320-
```
321-
mcp = FastMCP("My App",
322-
auth_server_provider=MyOAuthServerProvider(),
323-
auth=AuthSettings(
324-
issuer_url="https://myapp.com",
325-
revocation_options=RevocationOptions(
326-
enabled=True,
327-
),
328-
client_registration_options=ClientRegistrationOptions(
329-
enabled=True,
330-
valid_scopes=["myscope", "myotherscope"],
331-
default_scopes=["myscope"],
332-
),
333-
required_scopes=["myscope"],
320+
```python
321+
from mcp import FastMCP
322+
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
323+
from mcp.server.auth.settings import (
324+
AuthSettings,
325+
ClientRegistrationOptions,
326+
RevocationOptions,
327+
)
328+
329+
330+
class MyOAuthServerProvider(OAuthAuthorizationServerProvider):
331+
# See an example on how to implement at `examples/servers/simple-auth`
332+
...
333+
334+
335+
mcp = FastMCP(
336+
"My App",
337+
auth_server_provider=MyOAuthServerProvider(),
338+
auth=AuthSettings(
339+
issuer_url="https://myapp.com",
340+
revocation_options=RevocationOptions(
341+
enabled=True,
342+
),
343+
client_registration_options=ClientRegistrationOptions(
344+
enabled=True,
345+
valid_scopes=["myscope", "myotherscope"],
346+
default_scopes=["myscope"],
334347
),
348+
required_scopes=["myscope"],
349+
),
335350
)
336351
```
337352

338-
See [OAuthServerProvider](src/mcp/server/auth/provider.py) for more details.
353+
See [OAuthAuthorizationServerProvider](src/mcp/server/auth/provider.py) for more details.
339354

340355
## Running Your Server
341356

@@ -462,15 +477,12 @@ For low level server with Streamable HTTP implementations, see:
462477
- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)
463478
- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)
464479

465-
466-
467480
The streamable HTTP transport supports:
468481
- Stateful and stateless operation modes
469482
- Resumability with event stores
470-
- JSON or SSE response formats
483+
- JSON or SSE response formats
471484
- Better scalability for multi-node deployments
472485

473-
474486
### Mounting to an Existing ASGI Server
475487

476488
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).

src/mcp/client/session.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,18 @@ def __init__(
116116
self._message_handler = message_handler or _default_message_handler
117117

118118
async def initialize(self) -> types.InitializeResult:
119-
sampling = types.SamplingCapability()
120-
roots = types.RootsCapability(
119+
sampling = (
120+
types.SamplingCapability()
121+
if self._sampling_callback is not _default_sampling_callback
122+
else None
123+
)
124+
roots = (
121125
# TODO: Should this be based on whether we
122126
# _will_ send notifications, or only whether
123127
# they're supported?
124-
listChanged=True,
128+
types.RootsCapability(listChanged=True)
129+
if self._list_roots_callback is not _default_list_roots_callback
130+
else None
125131
)
126132

127133
result = await self.send_request(

src/mcp/client/stdio/__init__.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,28 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
108108
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
109109
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
110110

111-
command = _get_executable_command(server.command)
112-
113-
# Open process with stderr piped for capture
114-
process = await _create_platform_compatible_process(
115-
command=command,
116-
args=server.args,
117-
env=(
118-
{**get_default_environment(), **server.env}
119-
if server.env is not None
120-
else get_default_environment()
121-
),
122-
errlog=errlog,
123-
cwd=server.cwd,
124-
)
111+
try:
112+
command = _get_executable_command(server.command)
113+
114+
# Open process with stderr piped for capture
115+
process = await _create_platform_compatible_process(
116+
command=command,
117+
args=server.args,
118+
env=(
119+
{**get_default_environment(), **server.env}
120+
if server.env is not None
121+
else get_default_environment()
122+
),
123+
errlog=errlog,
124+
cwd=server.cwd,
125+
)
126+
except OSError:
127+
# Clean up streams if process creation fails
128+
await read_stream.aclose()
129+
await write_stream.aclose()
130+
await read_stream_writer.aclose()
131+
await write_stream_reader.aclose()
132+
raise
125133

126134
async def stdout_reader():
127135
assert process.stdout, "Opened process is missing stdout"
@@ -177,12 +185,18 @@ async def stdin_writer():
177185
yield read_stream, write_stream
178186
finally:
179187
# Clean up process to prevent any dangling orphaned processes
180-
if sys.platform == "win32":
181-
await terminate_windows_process(process)
182-
else:
183-
process.terminate()
188+
try:
189+
if sys.platform == "win32":
190+
await terminate_windows_process(process)
191+
else:
192+
process.terminate()
193+
except ProcessLookupError:
194+
# Process already exited, which is fine
195+
pass
184196
await read_stream.aclose()
185197
await write_stream.aclose()
198+
await read_stream_writer.aclose()
199+
await write_stream_reader.aclose()
186200

187201

188202
def _get_executable_command(command: str) -> str:

src/mcp/server/lowlevel/server.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def __init__(
147147
}
148148
self.notification_handlers: dict[type, Callable[..., Awaitable[None]]] = {}
149149
self.notification_options = NotificationOptions()
150-
logger.debug(f"Initializing server '{name}'")
150+
logger.debug("Initializing server %r", name)
151151

152152
def create_initialization_options(
153153
self,
@@ -510,7 +510,7 @@ async def run(
510510

511511
async with anyio.create_task_group() as tg:
512512
async for message in session.incoming_messages:
513-
logger.debug(f"Received message: {message}")
513+
logger.debug("Received message: %s", message)
514514

515515
tg.start_soon(
516516
self._handle_message,
@@ -543,7 +543,9 @@ async def _handle_message(
543543
await self._handle_notification(notify)
544544

545545
for warning in w:
546-
logger.info(f"Warning: {warning.category.__name__}: {warning.message}")
546+
logger.info(
547+
"Warning: %s: %s", warning.category.__name__, warning.message
548+
)
547549

548550
async def _handle_request(
549551
self,
@@ -553,10 +555,9 @@ async def _handle_request(
553555
lifespan_context: LifespanResultT,
554556
raise_exceptions: bool,
555557
):
556-
logger.info(f"Processing request of type {type(req).__name__}")
557-
if type(req) in self.request_handlers:
558-
handler = self.request_handlers[type(req)]
559-
logger.debug(f"Dispatching request of type {type(req).__name__}")
558+
logger.info("Processing request of type %s", type(req).__name__)
559+
if handler := self.request_handlers.get(type(req)): # type: ignore
560+
logger.debug("Dispatching request of type %s", type(req).__name__)
560561

561562
token = None
562563
try:
@@ -602,16 +603,13 @@ async def _handle_request(
602603
logger.debug("Response sent")
603604

604605
async def _handle_notification(self, notify: Any):
605-
if type(notify) in self.notification_handlers:
606-
assert type(notify) in self.notification_handlers
607-
608-
handler = self.notification_handlers[type(notify)]
609-
logger.debug(f"Dispatching notification of type {type(notify).__name__}")
606+
if handler := self.notification_handlers.get(type(notify)): # type: ignore
607+
logger.debug("Dispatching notification of type %s", type(notify).__name__)
610608

611609
try:
612610
await handler(notify)
613-
except Exception as err:
614-
logger.error(f"Uncaught exception in notification handler: {err}")
611+
except Exception:
612+
logger.exception("Uncaught exception in notification handler")
615613

616614

617615
async def _ping_handler(request: types.PingRequest) -> types.ServerResult:

src/mcp/server/streamable_http.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,8 @@ async def _handle_post_request(
397397
await response(scope, receive, send)
398398

399399
# Process the message after sending the response
400-
session_message = SessionMessage(message)
400+
metadata = ServerMessageMetadata(request_context=request)
401+
session_message = SessionMessage(message, metadata=metadata)
401402
await writer.send(session_message)
402403

403404
return
@@ -412,7 +413,8 @@ async def _handle_post_request(
412413

413414
if self.is_json_response_enabled:
414415
# Process the message
415-
session_message = SessionMessage(message)
416+
metadata = ServerMessageMetadata(request_context=request)
417+
session_message = SessionMessage(message, metadata=metadata)
416418
await writer.send(session_message)
417419
try:
418420
# Process messages from the request-specific stream
@@ -511,7 +513,8 @@ async def sse_writer():
511513
async with anyio.create_task_group() as tg:
512514
tg.start_soon(response, scope, receive, send)
513515
# Then send the message to be processed by the server
514-
session_message = SessionMessage(message)
516+
metadata = ServerMessageMetadata(request_context=request)
517+
session_message = SessionMessage(message, metadata=metadata)
515518
await writer.send(session_message)
516519
except Exception:
517520
logger.exception("SSE response error")

src/mcp/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ class RootsCapability(BaseModel):
218218

219219

220220
class SamplingCapability(BaseModel):
221-
"""Capability for logging operations."""
221+
"""Capability for sampling operations."""
222222

223223
model_config = ConfigDict(extra="allow")
224224

0 commit comments

Comments
 (0)