Skip to content

Commit 6bf73cd

Browse files
committed
Merge branch 'main' into outputSchema
2 parents b738f1b + 05b7156 commit 6bf73cd

31 files changed

+1006
-118
lines changed

.github/workflows/publish-docs-manually.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
uses: astral-sh/setup-uv@v3
2020
with:
2121
enable-cache: true
22+
version: 0.7.2
2223

2324
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
2425
- uses: actions/cache@v4

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
uses: astral-sh/setup-uv@v3
1717
with:
1818
enable-cache: true
19+
version: 0.7.2
1920

2021
- name: Set up Python 3.12
2122
run: uv python install 3.12
@@ -67,6 +68,7 @@ jobs:
6768
uses: astral-sh/setup-uv@v3
6869
with:
6970
enable-cache: true
71+
version: 0.7.2
7072

7173
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
7274
- uses: actions/cache@v4

.github/workflows/shared.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
uses: astral-sh/setup-uv@v3
1414
with:
1515
enable-cache: true
16+
version: 0.7.2
1617

1718
- name: Install the project
1819
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -29,6 +30,7 @@ jobs:
2930
uses: astral-sh/setup-uv@v3
3031
with:
3132
enable-cache: true
33+
version: 0.7.2
3234

3335
- name: Install the project
3436
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -50,6 +52,7 @@ jobs:
5052
uses: astral-sh/setup-uv@v3
5153
with:
5254
enable-cache: true
55+
version: 0.7.2
5356

5457
- name: Install the project
5558
run: uv sync --frozen --all-extras --dev --python ${{ matrix.python-version }}

README.md

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ from dataclasses import dataclass
160160

161161
from fake_database import Database # Replace with your actual DB type
162162

163-
from mcp.server.fastmcp import Context, FastMCP
163+
from mcp.server.fastmcp import FastMCP
164164

165165
# Create a named server
166166
mcp = FastMCP("My App")
@@ -192,9 +192,10 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
192192

193193
# Access type-safe lifespan context in tools
194194
@mcp.tool()
195-
def query_db(ctx: Context) -> str:
195+
def query_db() -> str:
196196
"""Tool that uses initialized resources"""
197-
db = ctx.request_context.lifespan_context.db
197+
ctx = mcp.get_context()
198+
db = ctx.request_context.lifespan_context["db"]
198199
return db.query()
199200
```
200201

@@ -314,27 +315,42 @@ async def long_task(files: list[str], ctx: Context) -> str:
314315
Authentication can be used by servers that want to expose tools accessing protected resources.
315316

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

319-
```
320-
mcp = FastMCP("My App",
321-
auth_server_provider=MyOAuthServerProvider(),
322-
auth=AuthSettings(
323-
issuer_url="https://myapp.com",
324-
revocation_options=RevocationOptions(
325-
enabled=True,
326-
),
327-
client_registration_options=ClientRegistrationOptions(
328-
enabled=True,
329-
valid_scopes=["myscope", "myotherscope"],
330-
default_scopes=["myscope"],
331-
),
332-
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"],
333347
),
348+
required_scopes=["myscope"],
349+
),
334350
)
335351
```
336352

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

339355
## Running Your Server
340356

@@ -461,15 +477,12 @@ For low level server with Streamable HTTP implementations, see:
461477
- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)
462478
- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)
463479

464-
465-
466480
The streamable HTTP transport supports:
467481
- Stateful and stateless operation modes
468482
- Resumability with event stores
469-
- JSON or SSE response formats
483+
- JSON or SSE response formats
470484
- Better scalability for multi-node deployments
471485

472-
473486
### Mounting to an Existing ASGI Server
474487

475488
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
@@ -631,7 +644,7 @@ server = Server("example-server", lifespan=server_lifespan)
631644
# Access lifespan context in handlers
632645
@server.call_tool()
633646
async def query_db(name: str, arguments: dict) -> list:
634-
ctx = server.get_context()
647+
ctx = server.request_context
635648
db = ctx.lifespan_context["db"]
636649
return await db.query(arguments["query"])
637650
```

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ mcp = "mcp.cli:app [cli]"
4545
[tool.uv]
4646
resolution = "lowest-direct"
4747
default-groups = ["dev", "docs"]
48+
required-version = ">=0.7.2"
4849

4950
[dependency-groups]
5051
dev = [
@@ -56,6 +57,7 @@ dev = [
5657
"pytest-xdist>=3.6.1",
5758
"pytest-examples>=0.0.14",
5859
"pytest-pretty>=1.2.0",
60+
"inline-snapshot>=0.23.0",
5961
]
6062
docs = [
6163
"mkdocs>=1.6.1",
@@ -64,7 +66,6 @@ docs = [
6466
"mkdocstrings-python>=1.12.2",
6567
]
6668

67-
6869
[build-system]
6970
requires = ["hatchling", "uv-dynamic-versioning"]
7071
build-backend = "hatchling.build"

src/mcp/client/session.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,18 @@ def __init__(
150150
)
151151

152152
async def initialize(self) -> types.InitializeResult:
153-
sampling = types.SamplingCapability()
154-
roots = types.RootsCapability(
153+
sampling = (
154+
types.SamplingCapability()
155+
if self._sampling_callback is not _default_sampling_callback
156+
else None
157+
)
158+
roots = (
155159
# TODO: Should this be based on whether we
156160
# _will_ send notifications, or only whether
157161
# they're supported?
158-
listChanged=True,
162+
types.RootsCapability(listChanged=True)
163+
if self._list_roots_callback is not _default_list_roots_callback
164+
else None
159165
)
160166

161167
result = await self.send_request(

src/mcp/client/session_group.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ async def __aexit__(
154154
for exit_stack in self._session_exit_stacks.values():
155155
tg.start_soon(exit_stack.aclose)
156156

157-
158157
@property
159158
def sessions(self) -> list[mcp.ClientSession]:
160159
"""Returns the list of sessions being managed."""

src/mcp/client/sse.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def sse_client(
5353

5454
async with anyio.create_task_group() as tg:
5555
try:
56-
logger.info(f"Connecting to SSE endpoint: {remove_request_params(url)}")
56+
logger.debug(f"Connecting to SSE endpoint: {remove_request_params(url)}")
5757
async with httpx_client_factory(headers=headers, auth=auth) as client:
5858
async with aconnect_sse(
5959
client,
@@ -73,7 +73,7 @@ async def sse_reader(
7373
match sse.event:
7474
case "endpoint":
7575
endpoint_url = urljoin(url, sse.data)
76-
logger.info(
76+
logger.debug(
7777
f"Received endpoint URL: {endpoint_url}"
7878
)
7979

@@ -146,7 +146,7 @@ async def post_writer(endpoint_url: str):
146146
await write_stream.aclose()
147147

148148
endpoint_url = await tg.start(sse_reader)
149-
logger.info(
149+
logger.debug(
150150
f"Starting post writer with endpoint URL: {endpoint_url}"
151151
)
152152
tg.start_soon(post_writer, endpoint_url)

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/client/streamable_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ async def streamablehttp_client(
463463

464464
async with anyio.create_task_group() as tg:
465465
try:
466-
logger.info(f"Connecting to StreamableHTTP endpoint: {url}")
466+
logger.debug(f"Connecting to StreamableHTTP endpoint: {url}")
467467

468468
async with httpx_client_factory(
469469
headers=transport.request_headers,

src/mcp/server/auth/routes.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -147,31 +147,15 @@ def create_auth_routes(
147147
return routes
148148

149149

150-
def modify_url_path(url: AnyHttpUrl, path_mapper: Callable[[str], str]) -> AnyHttpUrl:
151-
return AnyHttpUrl.build(
152-
scheme=url.scheme,
153-
username=url.username,
154-
password=url.password,
155-
host=url.host,
156-
port=url.port,
157-
path=path_mapper(url.path or ""),
158-
query=url.query,
159-
fragment=url.fragment,
160-
)
161-
162-
163150
def build_metadata(
164151
issuer_url: AnyHttpUrl,
165152
service_documentation_url: AnyHttpUrl | None,
166153
client_registration_options: ClientRegistrationOptions,
167154
revocation_options: RevocationOptions,
168155
) -> OAuthMetadata:
169-
authorization_url = modify_url_path(
170-
issuer_url, lambda path: path.rstrip("/") + AUTHORIZATION_PATH.lstrip("/")
171-
)
172-
token_url = modify_url_path(
173-
issuer_url, lambda path: path.rstrip("/") + TOKEN_PATH.lstrip("/")
174-
)
156+
authorization_url = AnyHttpUrl(str(issuer_url).rstrip("/") + AUTHORIZATION_PATH)
157+
token_url = AnyHttpUrl(str(issuer_url).rstrip("/") + TOKEN_PATH)
158+
175159
# Create metadata
176160
metadata = OAuthMetadata(
177161
issuer=issuer_url,
@@ -193,14 +177,14 @@ def build_metadata(
193177

194178
# Add registration endpoint if supported
195179
if client_registration_options.enabled:
196-
metadata.registration_endpoint = modify_url_path(
197-
issuer_url, lambda path: path.rstrip("/") + REGISTRATION_PATH.lstrip("/")
180+
metadata.registration_endpoint = AnyHttpUrl(
181+
str(issuer_url).rstrip("/") + REGISTRATION_PATH
198182
)
199183

200184
# Add revocation endpoint if supported
201185
if revocation_options.enabled:
202-
metadata.revocation_endpoint = modify_url_path(
203-
issuer_url, lambda path: path.rstrip("/") + REVOCATION_PATH.lstrip("/")
186+
metadata.revocation_endpoint = AnyHttpUrl(
187+
str(issuer_url).rstrip("/") + REVOCATION_PATH
204188
)
205189
metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"]
206190

0 commit comments

Comments
 (0)