Skip to content

Commit e42dbf5

Browse files
committed
Merge remote-tracking branch 'origin/main' into praboud/auth
2 parents 76ddc65 + 9ae4df8 commit e42dbf5

40 files changed

+651
-250
lines changed

README.md

Lines changed: 162 additions & 67 deletions
Large diffs are not rendered by default.

RELEASE.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
## Bumping Dependencies
44

5-
1. Change dependency
6-
2. Upgrade lock with `uv lock --resolution lowest-direct
5+
1. Change dependency version in `pyproject.toml`
6+
2. Upgrade lock with `uv lock --resolution lowest-direct`
77

88
## Major or Minor Release
99

10-
1. Create a release branch named `vX.Y.Z` where `X.Y.Z` is the version.
11-
2. Bump version number on release branch.
12-
3. Create an annotated, signed tag: `git tag -s -a vX.Y.Z`
13-
4. Create a github release using `gh release create` and publish it.
14-
5. Have the release flow being reviewed.
15-
7. Bump version number on `main` to the next version followed by `.dev`, e.g. `v0.4.0.dev`.
10+
Create a GitHub release via UI with the tag being `vX.Y.Z` where `X.Y.Z` is the version,
11+
and the release title being the same. Then ask someone to review the release.
12+
13+
The package version will be set automatically from the tag.

pyproject.toml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mcp"
3-
version = "1.4.0.dev0"
3+
dynamic = ["version"]
44
description = "Model Context Protocol SDK"
55
readme = "README.md"
66
requires-python = ">=3.10"
@@ -48,12 +48,21 @@ dev-dependencies = [
4848
"ruff>=0.8.5",
4949
"trio>=0.26.2",
5050
"pytest-xdist>=3.6.1",
51+
"pytest-examples>=0.0.14",
5152
]
5253

5354
[build-system]
54-
requires = ["hatchling"]
55+
requires = ["hatchling", "uv-dynamic-versioning"]
5556
build-backend = "hatchling.build"
5657

58+
[tool.hatch.version]
59+
source = "uv-dynamic-versioning"
60+
61+
[tool.uv-dynamic-versioning]
62+
vcs = "git"
63+
style = "pep440"
64+
bump = true
65+
5766
[project.urls]
5867
Homepage = "https://modelcontextprotocol.io"
5968
Repository = "https://github.com/modelcontextprotocol/python-sdk"
@@ -66,12 +75,11 @@ packages = ["src/mcp"]
6675
include = ["src/mcp", "tests"]
6776
venvPath = "."
6877
venv = ".venv"
69-
strict = [
70-
"src/mcp/server/fastmcp/tools/base.py",
71-
]
78+
strict = ["src/mcp/**/*.py"]
79+
exclude = ["src/mcp/types.py"]
7280

7381
[tool.ruff.lint]
74-
select = ["E", "F", "I"]
82+
select = ["E", "F", "I", "UP"]
7583
ignore = []
7684

7785
[tool.ruff]

src/mcp/cli/claude.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Claude app integration utilities."""
22

33
import json
4+
import os
45
import sys
56
from pathlib import Path
7+
from typing import Any
68

79
from mcp.server.fastmcp.utilities.logging import get_logger
810

@@ -17,6 +19,10 @@ def get_claude_config_path() -> Path | None:
1719
path = Path(Path.home(), "AppData", "Roaming", "Claude")
1820
elif sys.platform == "darwin":
1921
path = Path(Path.home(), "Library", "Application Support", "Claude")
22+
elif sys.platform.startswith("linux"):
23+
path = Path(
24+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude"
25+
)
2026
else:
2127
return None
2228

@@ -111,10 +117,7 @@ def update_claude_config(
111117
# Add fastmcp run command
112118
args.extend(["mcp", "run", file_spec])
113119

114-
server_config = {
115-
"command": "uv",
116-
"args": args,
117-
}
120+
server_config: dict[str, Any] = {"command": "uv", "args": args}
118121

119122
# Add environment variables if specified
120123
if env_vars:

src/mcp/cli/cli.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,14 @@ def run(
294294
) -> None:
295295
"""Run a MCP server.
296296
297-
The server can be specified in two ways:
298-
1. Module approach: server.py - runs the module directly, expecting a server.run()
299-
call
300-
2. Import approach: server.py:app - imports and runs the specified server object
297+
The server can be specified in two ways:\n
298+
1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n
299+
2. Import approach: server.py:app - imports and runs the specified server object.\n\n
301300
302301
Note: This command runs the server directly. You are responsible for ensuring
303-
all dependencies are available. For dependency management, use mcp install
304-
or mcp dev instead.
305-
"""
302+
all dependencies are available.\n
303+
For dependency management, use `mcp install` or `mcp dev` instead.
304+
""" # noqa: E501
306305
file, server_object = _parse_file_path(file_spec)
307306

308307
logger.debug(

src/mcp/client/__main__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from urllib.parse import urlparse
66

77
import anyio
8+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
89

910
from mcp.client.session import ClientSession
1011
from mcp.client.sse import sse_client
1112
from mcp.client.stdio import StdioServerParameters, stdio_client
13+
from mcp.types import JSONRPCMessage
1214

1315
if not sys.warnoptions:
1416
import warnings
@@ -29,7 +31,10 @@ async def receive_loop(session: ClientSession):
2931
logger.info("Received message from server: %s", message)
3032

3133

32-
async def run_session(read_stream, write_stream):
34+
async def run_session(
35+
read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
36+
write_stream: MemoryObjectSendStream[JSONRPCMessage],
37+
):
3338
async with (
3439
ClientSession(read_stream, write_stream) as session,
3540
anyio.create_task_group() as tg,

src/mcp/client/session.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ async def __call__(
2424
) -> types.ListRootsResult | types.ErrorData: ...
2525

2626

27+
class LoggingFnT(Protocol):
28+
async def __call__(
29+
self,
30+
params: types.LoggingMessageNotificationParams,
31+
) -> None: ...
32+
33+
2734
async def _default_sampling_callback(
2835
context: RequestContext["ClientSession", Any],
2936
params: types.CreateMessageRequestParams,
@@ -43,6 +50,12 @@ async def _default_list_roots_callback(
4350
)
4451

4552

53+
async def _default_logging_callback(
54+
params: types.LoggingMessageNotificationParams,
55+
) -> None:
56+
pass
57+
58+
4659
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(
4760
types.ClientResult | types.ErrorData
4861
)
@@ -64,6 +77,7 @@ def __init__(
6477
read_timeout_seconds: timedelta | None = None,
6578
sampling_callback: SamplingFnT | None = None,
6679
list_roots_callback: ListRootsFnT | None = None,
80+
logging_callback: LoggingFnT | None = None,
6781
) -> None:
6882
super().__init__(
6983
read_stream,
@@ -74,20 +88,15 @@ def __init__(
7488
)
7589
self._sampling_callback = sampling_callback or _default_sampling_callback
7690
self._list_roots_callback = list_roots_callback or _default_list_roots_callback
91+
self._logging_callback = logging_callback or _default_logging_callback
7792

7893
async def initialize(self) -> types.InitializeResult:
79-
sampling = (
80-
types.SamplingCapability() if self._sampling_callback is not None else None
81-
)
82-
roots = (
83-
types.RootsCapability(
84-
# TODO: Should this be based on whether we
85-
# _will_ send notifications, or only whether
86-
# they're supported?
87-
listChanged=True,
88-
)
89-
if self._list_roots_callback is not None
90-
else None
94+
sampling = types.SamplingCapability()
95+
roots = types.RootsCapability(
96+
# TODO: Should this be based on whether we
97+
# _will_ send notifications, or only whether
98+
# they're supported?
99+
listChanged=True,
91100
)
92101

93102
result = await self.send_request(
@@ -327,3 +336,13 @@ async def _received_request(
327336
return await responder.respond(
328337
types.ClientResult(root=types.EmptyResult())
329338
)
339+
340+
async def _received_notification(
341+
self, notification: types.ServerNotification
342+
) -> None:
343+
"""Handle notifications from the server."""
344+
match notification.root:
345+
case types.LoggingMessageNotification(params=params):
346+
await self._logging_callback(params)
347+
case _:
348+
pass

src/mcp/client/sse.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ async def sse_reader(
127127
continue
128128

129129
await read_stream_writer.send(message)
130+
case _:
131+
logger.warning(
132+
f"Unknown SSE event: {sse.event}"
133+
)
130134
except Exception as exc:
131135
logger.error(f"Error in sse_reader: {exc}")
132136
await read_stream_writer.send(exc)

src/mcp/client/stdio.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
22
import sys
33
from contextlib import asynccontextmanager
4-
from typing import Literal
4+
from pathlib import Path
5+
from typing import Literal, TextIO
56

67
import anyio
78
import anyio.lowlevel
@@ -66,6 +67,9 @@ class StdioServerParameters(BaseModel):
6667
If not specified, the result of get_default_environment() will be used.
6768
"""
6869

70+
cwd: str | Path | None = None
71+
"""The working directory to use when spawning the process."""
72+
6973
encoding: str = "utf-8"
7074
"""
7175
The text encoding used when sending/receiving messages to the server
@@ -83,7 +87,7 @@ class StdioServerParameters(BaseModel):
8387

8488

8589
@asynccontextmanager
86-
async def stdio_client(server: StdioServerParameters):
90+
async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr):
8791
"""
8892
Client transport for stdio: this will connect to a server by spawning a
8993
process and communicating with it over stdin/stdout.
@@ -99,8 +103,13 @@ async def stdio_client(server: StdioServerParameters):
99103

100104
process = await anyio.open_process(
101105
[server.command, *server.args],
102-
env=server.env if server.env is not None else get_default_environment(),
103-
stderr=sys.stderr,
106+
env=(
107+
{**get_default_environment(), **server.env}
108+
if server.env is not None
109+
else get_default_environment()
110+
),
111+
stderr=errlog,
112+
cwd=server.cwd,
104113
)
105114

106115
async def stdout_reader():

src/mcp/client/websocket.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
3+
from collections.abc import AsyncGenerator
34
from contextlib import asynccontextmanager
4-
from typing import AsyncGenerator
55

66
import anyio
77
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
@@ -39,6 +39,11 @@ async def websocket_client(
3939
# Create two in-memory streams:
4040
# - One for incoming messages (read_stream, written by ws_reader)
4141
# - One for outgoing messages (write_stream, read by ws_writer)
42+
read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception]
43+
read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception]
44+
write_stream: MemoryObjectSendStream[types.JSONRPCMessage]
45+
write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage]
46+
4247
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
4348
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
4449

0 commit comments

Comments
 (0)